@mindexec/cli 0.2.477 → 0.2.478

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 (145) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7690 -7690
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  64. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  65. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  66. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  67. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  68. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  69. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  70. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  71. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  72. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  73. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  74. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  75. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10506
  76. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  77. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  78. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  79. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  80. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  81. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  82. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  100. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  101. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  102. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  103. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  104. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  105. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  106. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  107. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  108. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  109. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  110. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  111. package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
  112. package/wwwroot/_framework/{MindExecution.Core.ty1xewqp36.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
  113. package/wwwroot/_framework/{MindExecution.Kernel.h7rqjoo69o.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
  114. package/wwwroot/_framework/{MindExecution.Plugins.Admin.wrfrcvhjde.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
  115. package/wwwroot/_framework/{MindExecution.Plugins.Business.csbcyjurdh.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
  116. package/wwwroot/_framework/{MindExecution.Plugins.Concept.buomiysqjn.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
  117. package/wwwroot/_framework/{MindExecution.Plugins.Directory.7k00i2v769.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
  118. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.85i5ocraro.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
  119. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.nbolrfakte.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
  120. package/wwwroot/_framework/{MindExecution.Shared.nqh4porv7x.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
  121. package/wwwroot/_framework/{MindExecution.Web.211q3bf4mr.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
  122. package/wwwroot/_framework/blazor.boot.json +23 -23
  123. package/wwwroot/_headers +40 -40
  124. package/wwwroot/_redirects +1 -1
  125. package/wwwroot/index.html +772 -772
  126. package/wwwroot/js/marketing-tool.js +180 -180
  127. package/wwwroot/manifest.webmanifest +23 -23
  128. package/wwwroot/service-worker-assets.js +78 -106
  129. package/wwwroot/service-worker.js +33 -33
  130. package/tree-sitter-grammars/README.md +0 -18
  131. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  132. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  133. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  134. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  135. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  136. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  137. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  138. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  139. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  140. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  141. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  142. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
  143. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
  144. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
  145. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
@@ -3,769 +3,769 @@
3
3
  (function () {
4
4
  'use strict';
5
5
 
6
- let currentMenuElement = null; // DOM overlay element
7
- let currentMenuNodeId = null;
8
- let _module = null;
9
- const MEMO_COLORS = [
10
- { key: 'coral', swatch: '#ff8052', ring: 'rgba(255, 128, 82, 0.22)' },
11
- { key: 'amber', swatch: '#f59e0b', ring: 'rgba(245, 158, 11, 0.22)' },
12
- { key: 'sun', swatch: '#ca8a04', ring: 'rgba(202, 138, 4, 0.22)' },
13
- { key: 'sage', swatch: '#65a30d', ring: 'rgba(101, 163, 13, 0.22)' },
14
- { key: 'mint', swatch: '#10b981', ring: 'rgba(16, 185, 129, 0.22)' },
15
- { key: 'sky', swatch: '#3b82f6', ring: 'rgba(59, 130, 246, 0.22)' },
16
- { key: 'indigo', swatch: '#6366f1', ring: 'rgba(99, 102, 241, 0.22)' },
17
- { key: 'violet', swatch: '#8b5cf6', ring: 'rgba(139, 92, 246, 0.22)' },
18
- { key: 'rose', swatch: '#f43f5e', ring: 'rgba(244, 63, 94, 0.22)' },
19
- { key: 'gray', swatch: '#6b7280', ring: 'rgba(107, 114, 128, 0.22)' },
20
- { key: 'slate', swatch: '#64748b', ring: 'rgba(100, 116, 139, 0.22)' }
21
- ];
22
- const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
23
- const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
24
-
25
- function ensureFeedbackStyles() {
26
- if (document.getElementById('mindmap-feedback-styles')) return;
27
-
28
- const style = document.createElement('style');
29
- style.id = 'mindmap-feedback-styles';
30
- style.textContent = `
31
- .mindmap-toast-stack {
32
- position: fixed;
33
- right: 22px;
34
- bottom: 24px;
35
- z-index: 2147483000;
36
- display: flex;
37
- flex-direction: column;
38
- gap: 10px;
39
- pointer-events: none;
40
- }
41
-
42
- .mindmap-toast {
43
- width: min(380px, calc(100vw - 32px));
44
- padding: 12px 14px;
45
- border-radius: 16px;
46
- background: rgba(15, 23, 42, 0.94);
47
- color: #f8fafc;
48
- box-shadow: 0 18px 42px rgba(15, 23, 42, 0.24);
49
- font: 600 13px/1.45 Inter, ui-sans-serif, system-ui, sans-serif;
50
- opacity: 0;
51
- transform: translateY(8px);
52
- transition: opacity 180ms ease, transform 180ms ease;
53
- pointer-events: auto;
54
- }
55
-
56
- .mindmap-toast.is-visible {
57
- opacity: 1;
58
- transform: translateY(0);
59
- }
60
-
61
- .mindmap-toast[data-kind="error"] {
62
- background: rgba(127, 29, 29, 0.94);
63
- }
64
-
65
- .mindmap-confirm-backdrop {
66
- position: fixed;
67
- inset: 0;
68
- z-index: 2147483001;
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
72
- padding: 24px;
73
- background: rgba(15, 23, 42, 0.20);
74
- backdrop-filter: blur(5px);
75
- }
76
-
77
- .mindmap-confirm-panel {
78
- width: min(440px, calc(100vw - 36px));
79
- border: 1px solid rgba(203, 213, 225, 0.86);
80
- border-radius: 22px;
81
- background: rgba(248, 250, 252, 0.98);
82
- box-shadow: 0 28px 72px rgba(15, 23, 42, 0.22);
83
- padding: 20px;
84
- color: #0f172a;
85
- font-family: Inter, ui-sans-serif, system-ui, sans-serif;
86
- }
87
-
88
- .mindmap-confirm-title {
89
- margin: 0;
90
- font-size: 15px;
91
- font-weight: 800;
92
- letter-spacing: 0.01em;
93
- }
94
-
95
- .mindmap-confirm-message {
96
- margin: 8px 0 0;
97
- color: #475569;
98
- font-size: 13px;
99
- line-height: 1.55;
100
- }
101
-
102
- .mindmap-confirm-actions {
103
- display: flex;
104
- justify-content: flex-end;
105
- gap: 10px;
106
- margin-top: 18px;
107
- }
108
-
109
- .mindmap-confirm-button {
110
- border: 0;
111
- border-radius: 999px;
112
- padding: 9px 15px;
113
- font-size: 13px;
114
- font-weight: 800;
115
- cursor: pointer;
116
- transition: transform 120ms ease, background 120ms ease;
117
- }
118
-
119
- .mindmap-confirm-button:active {
120
- transform: scale(0.98);
121
- }
122
-
123
- .mindmap-confirm-button.secondary {
124
- background: #e0f2fe;
125
- color: #075985;
126
- }
127
-
128
- .mindmap-confirm-button.primary {
129
- background: #0f172a;
130
- color: #ffffff;
131
- }
132
- `;
133
- document.head.appendChild(style);
134
- }
135
-
136
- function getToastStack() {
137
- ensureFeedbackStyles();
138
- let stack = document.querySelector('.mindmap-toast-stack');
139
- if (!stack) {
140
- stack = document.createElement('div');
141
- stack.className = 'mindmap-toast-stack';
142
- document.body.appendChild(stack);
143
- }
144
- return stack;
145
- }
146
-
147
- function showToast(message, options = {}) {
148
- const text = String(message || '').trim();
149
- if (!text || !document?.body) return;
150
-
151
- const toast = document.createElement('div');
152
- toast.className = 'mindmap-toast';
153
- toast.dataset.kind = options.kind || 'info';
154
- toast.textContent = text;
155
-
156
- getToastStack().appendChild(toast);
157
- requestAnimationFrame(() => toast.classList.add('is-visible'));
158
-
159
- window.setTimeout(() => {
160
- toast.classList.remove('is-visible');
161
- window.setTimeout(() => toast.remove(), 220);
162
- }, options.duration || 2600);
163
- }
164
-
165
- function showConfirmDialog(options = {}) {
166
- if (!document?.body) return Promise.resolve(false);
167
-
168
- ensureFeedbackStyles();
169
-
170
- return new Promise((resolve) => {
171
- const backdrop = document.createElement('div');
172
- backdrop.className = 'mindmap-confirm-backdrop';
173
-
174
- const panel = document.createElement('div');
175
- panel.className = 'mindmap-confirm-panel';
176
- panel.setAttribute('role', 'dialog');
177
- panel.setAttribute('aria-modal', 'true');
178
-
179
- const title = document.createElement('h3');
180
- title.className = 'mindmap-confirm-title';
181
- title.textContent = options.title || 'Confirm action';
182
-
183
- const message = document.createElement('p');
184
- message.className = 'mindmap-confirm-message';
185
- message.textContent = options.message || 'Do you want to continue?';
186
-
187
- const actions = document.createElement('div');
188
- actions.className = 'mindmap-confirm-actions';
189
-
190
- const cancel = document.createElement('button');
191
- cancel.type = 'button';
192
- cancel.className = 'mindmap-confirm-button secondary';
193
- cancel.textContent = options.cancelText || 'Cancel';
194
-
195
- const confirm = document.createElement('button');
196
- confirm.type = 'button';
197
- confirm.className = 'mindmap-confirm-button primary';
198
- confirm.textContent = options.confirmText || 'Confirm';
199
-
200
- const close = (value) => {
201
- backdrop.remove();
202
- document.removeEventListener('keydown', handleKeyDown, true);
203
- resolve(!!value);
204
- };
205
-
206
- const handleKeyDown = (event) => {
207
- if (event.key === 'Escape') {
208
- event.preventDefault();
209
- close(false);
210
- }
211
- };
212
-
213
- cancel.addEventListener('click', () => close(false));
214
- confirm.addEventListener('click', () => close(true));
215
- backdrop.addEventListener('click', (event) => {
216
- if (event.target === backdrop) close(false);
217
- });
218
- document.addEventListener('keydown', handleKeyDown, true);
219
-
220
- actions.appendChild(cancel);
221
- actions.appendChild(confirm);
222
- panel.appendChild(title);
223
- panel.appendChild(message);
224
- panel.appendChild(actions);
225
- backdrop.appendChild(panel);
226
- document.body.appendChild(backdrop);
227
- confirm.focus({ preventScroll: true });
228
- });
229
- }
230
-
231
- window.MindMapFeedback = window.MindMapFeedback || {};
232
- window.MindMapFeedback.toast = showToast;
233
- window.MindMapFeedback.confirm = showConfirmDialog;
234
-
235
- function init(module) {
236
- _module = module;
237
- }
238
-
239
- function ensureMetadata(model) {
240
- if (!model) return {};
241
- if (model.metadata && typeof model.metadata === 'object') return model.metadata;
242
- if (model.Metadata && typeof model.Metadata === 'object') {
243
- model.metadata = model.Metadata;
244
- return model.metadata;
245
- }
246
- model.metadata = {};
247
- model.Metadata = model.metadata;
248
- return model.metadata;
249
- }
250
-
251
- function getNodeSemanticType(model) {
252
- const metadata = ensureMetadata(model);
253
- return String(metadata.SemanticType || metadata.semanticType || '').trim();
254
- }
255
-
256
- function isAgentStyledMemoNode(model) {
257
- const semanticType = getNodeSemanticType(model);
258
- return semanticType === 'MindCanvasAgent'
259
- || semanticType === 'AgentCommand'
260
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
261
- }
262
-
263
- function isBusinessAutomationNode(model) {
264
- return getNodeSemanticType(model) === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
265
- }
266
-
267
- function getBusinessAutomationNodeKind(model) {
268
- const metadata = ensureMetadata(model);
269
- return String(metadata.AutomationNodeKind || metadata.automationNodeKind || '').trim().toLowerCase();
270
- }
271
-
272
- function getBusinessAutomationNodeRole(model) {
273
- const metadata = ensureMetadata(model);
274
- return String(metadata.AutomationNodeRole || metadata.automationNodeRole || '').trim().toLowerCase();
275
- }
276
-
277
- function isBusinessAutomationRootAiTaskNode(model) {
278
- if (!isBusinessAutomationNode(model) || getBusinessAutomationNodeKind(model) !== 'ai-task') {
279
- return false;
280
- }
281
-
282
- const role = getBusinessAutomationNodeRole(model);
283
- return role.length === 0 || role === 'root';
284
- }
285
-
286
- function isRemoteFleetMonitorNode(model) {
287
- return getNodeSemanticType(model) === REMOTE_FLEET_SEMANTIC_TYPE;
288
- }
289
-
290
- function canRunExecutableNode(model) {
291
- return isBusinessAutomationRootAiTaskNode(model);
292
- }
293
-
294
- function isExecutableNodeRunning(model) {
295
- const metadata = ensureMetadata(model);
296
- return String(metadata.AutomationRunState || '').trim().toLowerCase() === 'running';
297
- }
298
-
299
- function setExecutableNodeRunVisualState(nodeId, runState, statusLabel) {
300
- const normalizedNodeId = String(nodeId || '').trim();
301
- if (!normalizedNodeId || !_module?.nodeObjectsById) {
302
- return false;
303
- }
304
-
305
- const entry = _module.nodeObjectsById.get(normalizedNodeId);
306
- if (!entry?.model) {
307
- return false;
308
- }
309
-
310
- const metadata = ensureMetadata(entry.model);
311
- const normalizedRunState = String(runState || '').trim().toLowerCase();
312
- metadata.AutomationRunState = normalizedRunState || 'idle';
313
-
314
- if (normalizedRunState === 'running') {
315
- metadata.AutomationLastRunStatus = 'running';
316
- metadata.AutomationLastRunStatusLabel = statusLabel || 'Running';
317
- metadata.AutomationLastRunError = '';
318
- } else if (statusLabel) {
319
- metadata.AutomationLastRunStatusLabel = statusLabel;
320
- }
321
-
322
- entry.model.metadata = metadata;
323
- entry.model.Metadata = metadata;
324
-
325
- if (entry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
326
- try {
327
- entry.isCssDirty = true;
328
- window.MindMapCss3DManager.syncCss3dContent(entry.model, entry.cssObject);
329
- } finally {
330
- entry.isCssDirty = false;
331
- }
332
- }
333
-
334
- _module.lodRenderer?.requestVisualRefresh?.('node-run-visual-state');
335
- return true;
336
- }
337
-
338
- async function syncNodeRunResultStates(result) {
339
- if (!result) {
340
- return;
341
- }
342
-
343
- const nodeStates = Array.isArray(result.nodeStates)
344
- ? result.nodeStates
345
- : (result.agentNodeState ? [result.agentNodeState] : []);
346
-
347
- if (nodeStates.length > 0 && window.mindMap?.syncNodeStates) {
348
- await window.mindMap.syncNodeStates(nodeStates);
349
- }
350
- }
351
-
352
- function isTextFitNode(model) {
353
- const contentType = String(model?.contentType || '').toLowerCase();
354
- return contentType === 'text' || contentType === 'markdown' || contentType === 'note' || contentType === 'code';
355
- }
356
-
357
- function getNodeContentType(model) {
358
- return String(model?.contentType || '').trim().toLowerCase();
359
- }
360
-
361
- function showActionSuccessIcon(button) {
362
- const icon = button?.querySelector?.('i');
363
- if (!icon) return;
364
-
365
- const oldClass = icon.className;
366
- icon.className = 'fa-solid fa-check text-green-500';
367
- setTimeout(() => {
368
- icon.className = oldClass;
369
- }, 1000);
370
- }
371
-
372
- function getImageClipboardSourceCandidates(nodeModel) {
373
- const assetUrls = window.MindMapNodes?.getNodeImageAssetUrls?.(nodeModel) || {};
374
- const rawCandidates = [
375
- assetUrls.fullResUrl,
376
- assetUrls.originalUrl,
377
- assetUrls.originalPath,
378
- assetUrls.previewUrl,
379
- assetUrls.responseUrl,
380
- nodeModel?.response,
381
- nodeModel?.Response
382
- ];
383
-
384
- const candidates = [];
385
- rawCandidates.forEach(value => {
386
- const normalized = typeof value === 'string' ? value.trim() : '';
387
- if (!normalized || candidates.includes(normalized)) {
388
- return;
389
- }
390
-
391
- candidates.push(normalized);
392
- });
393
-
394
- return candidates;
395
- }
396
-
397
- function shouldResolveImageClipboardSourceBeforeFetch(src) {
398
- const normalized = typeof src === 'string' ? src.trim().toLowerCase() : '';
399
- if (!normalized || normalized.startsWith('blob:') || normalized.startsWith('data:') || normalized.startsWith('file:')) {
400
- return false;
401
- }
402
-
403
- return normalized.startsWith('/assets/')
404
- || normalized.startsWith('assets/')
405
- || normalized.includes('/assets/thumbs/')
406
- || (normalized.includes('/assets/') && !/\.[a-z0-9]{2,5}(?:[?#]|$)/i.test(normalized));
407
- }
408
-
409
- async function resolveClipboardImageSource(nodeModel, src) {
410
- if (!shouldResolveImageClipboardSourceBeforeFetch(src) || !_module?.dotNetHelper) {
411
- return src;
412
- }
413
-
414
- const nodeId = String(nodeModel?.id || nodeModel?.Id || '').trim();
415
- if (!nodeId) {
416
- return src;
417
- }
418
-
419
- try {
420
- const resolved = await _module.dotNetHelper.invokeMethodAsync('ResolveNodeAssetUrl', nodeId);
421
- const normalized = typeof resolved === 'string' ? resolved.trim() : '';
422
- return normalized || src;
423
- } catch (error) {
424
- console.warn('[MindMapMenuManager] Failed to resolve image clipboard source.', error);
425
- return src;
426
- }
427
- }
428
-
429
- async function fetchClipboardImageBlob(nodeModel) {
430
- const candidates = getImageClipboardSourceCandidates(nodeModel);
431
- let lastError = null;
432
-
433
- for (const src of candidates) {
434
- try {
435
- const resolvedSrc = await resolveClipboardImageSource(nodeModel, src);
436
- const response = await fetch(resolvedSrc, { cache: 'no-store' });
437
- if (!response.ok) {
438
- throw new Error(`HTTP ${response.status}`);
439
- }
440
-
441
- const blob = await response.blob();
442
- if (!(blob instanceof Blob) || blob.size <= 0) {
443
- throw new Error('Empty image blob.');
444
- }
445
-
446
- return { blob, src: resolvedSrc };
447
- } catch (error) {
448
- lastError = error;
449
- console.warn(`[MindMapMenuManager] Failed to fetch clipboard image from ${src}`, error);
450
- }
451
- }
452
-
453
- throw lastError || new Error('No image source was available for clipboard copy.');
454
- }
455
-
456
- async function decodeClipboardImageBlob(blob) {
457
- if (typeof createImageBitmap === 'function') {
458
- try {
459
- const bitmap = await createImageBitmap(blob);
460
- return {
461
- width: bitmap.width,
462
- height: bitmap.height,
463
- draw(ctx, width, height) {
464
- ctx.drawImage(bitmap, 0, 0, width, height);
465
- },
466
- dispose() {
467
- bitmap.close?.();
468
- }
469
- };
470
- } catch (error) {
471
- console.warn('[MindMapMenuManager] createImageBitmap failed for clipboard image conversion.', error);
472
- }
473
- }
474
-
475
- const objectUrl = URL.createObjectURL(blob);
476
- try {
477
- const image = await new Promise((resolve, reject) => {
478
- const img = new Image();
479
- img.decoding = 'async';
480
- img.onload = () => resolve(img);
481
- img.onerror = () => reject(new Error('Failed to decode image blob.'));
482
- img.src = objectUrl;
483
- });
484
-
485
- return {
486
- width: image.naturalWidth || image.width,
487
- height: image.naturalHeight || image.height,
488
- draw(ctx, width, height) {
489
- ctx.drawImage(image, 0, 0, width, height);
490
- },
491
- dispose() {
492
- image.remove?.();
493
- URL.revokeObjectURL(objectUrl);
494
- }
495
- };
496
- } catch (error) {
497
- URL.revokeObjectURL(objectUrl);
498
- throw error;
499
- }
500
- }
501
-
502
- async function convertBlobToPngForClipboard(blob) {
503
- const normalizedType = String(blob?.type || '').toLowerCase();
504
- if (normalizedType === 'image/png' && blob.size > 0) {
505
- return blob;
506
- }
507
-
508
- const decoded = await decodeClipboardImageBlob(blob);
509
- const width = Math.max(1, Math.round(decoded?.width || 0));
510
- const height = Math.max(1, Math.round(decoded?.height || 0));
511
-
512
- if (!width || !height) {
513
- decoded?.dispose?.();
514
- throw new Error('Decoded image had no dimensions.');
515
- }
516
-
517
- try {
518
- if (typeof OffscreenCanvas === 'function') {
519
- const canvas = new OffscreenCanvas(width, height);
520
- const ctx = canvas.getContext('2d');
521
- if (!ctx) {
522
- throw new Error('OffscreenCanvas 2D context is unavailable.');
523
- }
524
-
525
- decoded.draw(ctx, width, height);
526
- return await canvas.convertToBlob({ type: 'image/png' });
527
- }
528
-
529
- const canvas = document.createElement('canvas');
530
- canvas.width = width;
531
- canvas.height = height;
532
- const ctx = canvas.getContext('2d');
533
- if (!ctx) {
534
- throw new Error('Canvas 2D context is unavailable.');
535
- }
536
-
537
- decoded.draw(ctx, width, height);
538
- return await new Promise((resolve, reject) => {
539
- canvas.toBlob(result => {
540
- if (result instanceof Blob && result.size > 0) {
541
- resolve(result);
542
- return;
543
- }
544
-
545
- reject(new Error('Failed to encode clipboard image as PNG.'));
546
- }, 'image/png');
547
- });
548
- } finally {
549
- decoded?.dispose?.();
550
- }
551
- }
552
-
553
- async function writeImageBlobToClipboard(blob, fallbackText) {
554
- if (navigator.clipboard?.write && typeof ClipboardItem === 'function') {
555
- await navigator.clipboard.write([
556
- new ClipboardItem({ 'image/png': blob })
557
- ]);
558
- return 'image';
559
- }
560
-
561
- if (navigator.clipboard?.writeText && fallbackText) {
562
- await navigator.clipboard.writeText(fallbackText);
563
- return 'text';
564
- }
565
-
566
- throw new Error('Clipboard API is unavailable.');
567
- }
568
-
569
- function getNodeSourceFilePath(model) {
570
- return String(model?.sourceFilePath || model?.SourceFilePath || '').trim();
571
- }
572
-
573
- function getCompanyBindingValue(model, key) {
574
- const metadata = ensureMetadata(model);
575
- return String(metadata[key] || '').trim();
576
- }
577
-
578
- function isCompanyBoundNode(model) {
579
- return getCompanyBindingValue(model, 'CompanyCore.CompanyId').length > 0;
580
- }
581
-
582
- function getCompanyStatus(model) {
583
- return getCompanyBindingValue(model, 'CompanyCore.Status').toLowerCase();
584
- }
585
-
586
- function isPendingCompanyBinding(model) {
587
- const companyId = getCompanyBindingValue(model, 'CompanyCore.CompanyId');
588
- if (companyId.startsWith('pending:')) {
589
- return true;
590
- }
591
-
592
- const status = getCompanyStatus(model);
593
- const runId = getCompanyBindingValue(model, 'CompanyCore.RunId');
594
- return !companyId && status === 'queued' && !runId;
595
- }
596
-
597
- function applyCompanyBindingValues(model, values) {
598
- const metadata = ensureMetadata(model);
599
- Object.entries(values || {}).forEach(([key, value]) => {
600
- if (value === null || value === undefined || value === '') {
601
- delete metadata[key];
602
- return;
603
- }
604
-
605
- metadata[key] = String(value);
606
- });
607
- model.metadata = metadata;
608
- model.Metadata = metadata;
609
- }
610
-
611
- function applyOptimisticCompanyRunState(nodeEntry, nodeId, executionMode, enableSubAgents) {
612
- if (!nodeEntry?.model) return;
613
-
614
- applyCompanyBindingValues(nodeEntry.model, {
615
- 'CompanyCore.CompanyId': `pending:${nodeId}`,
616
- 'CompanyCore.ExecutionMode': executionMode || 'Auto',
617
- 'CompanyCore.SubAgentsEnabled': enableSubAgents ? 'true' : 'false',
618
- 'CompanyCore.Status': 'Queued',
619
- 'CompanyCore.LastMessage': 'Starting company from this note...',
620
- 'CompanyCore.PendingApprovalCount': '0',
621
- 'CompanyCore.RunningAgentCount': '0',
622
- 'CompanyCore.WorkItemCount': '0',
623
- 'CompanyCore.ArtifactCount': '0',
624
- 'CompanyCore.GrowthResultCount': '0',
625
- 'CompanyCore.RevenueResultCount': '0'
626
- });
627
- }
628
-
629
- function applyCompanyRunResult(nodeEntry, result, fallbackExecutionMode, fallbackEnableSubAgents) {
630
- if (!nodeEntry?.model) return;
631
-
632
- const companyId = String(result?.companyId || '').trim();
633
- const status = String(result?.status || '').trim();
634
- if (!companyId) {
635
- applyCompanyBindingValues(nodeEntry.model, {
636
- 'CompanyCore.CompanyId': null,
637
- 'CompanyCore.RunId': null,
638
- 'CompanyCore.ExecutionMode': result?.executionMode || fallbackExecutionMode || 'Auto',
639
- 'CompanyCore.SubAgentsEnabled': typeof result?.subAgentsEnabled === 'boolean'
640
- ? (result.subAgentsEnabled ? 'true' : 'false')
641
- : (fallbackEnableSubAgents ? 'true' : 'false'),
642
- 'CompanyCore.Status': status || 'Failed',
643
- 'CompanyCore.LastMessage': result?.lastMessage || 'Company did not start from this note.',
644
- 'CompanyCore.PendingApprovalCount': String(result?.pendingApprovalCount ?? 0),
645
- 'CompanyCore.RunningAgentCount': String(result?.runningAgentCount ?? 0),
646
- 'CompanyCore.WorkItemCount': String(result?.workItemCount ?? 0),
647
- 'CompanyCore.ArtifactCount': String(result?.artifactCount ?? 0),
648
- 'CompanyCore.GrowthResultCount': String(result?.growthResultCount ?? 0),
649
- 'CompanyCore.RevenueResultCount': String(result?.revenueResultCount ?? 0),
650
- 'CompanyCore.LastSnapshotAt': result?.lastSnapshotAt || null
651
- });
652
- return;
653
- }
654
-
655
- applyCompanyBindingValues(nodeEntry.model, {
656
- 'CompanyCore.CompanyId': companyId,
657
- 'CompanyCore.RunId': result?.runId || null,
658
- 'CompanyCore.ExecutionMode': result?.executionMode || fallbackExecutionMode || 'Auto',
659
- 'CompanyCore.SubAgentsEnabled': typeof result?.subAgentsEnabled === 'boolean'
660
- ? (result.subAgentsEnabled ? 'true' : 'false')
661
- : (fallbackEnableSubAgents ? 'true' : 'false'),
662
- 'CompanyCore.Status': status || 'Queued',
663
- 'CompanyCore.PendingApprovalCount': String(result?.pendingApprovalCount ?? 0),
664
- 'CompanyCore.RunningAgentCount': String(result?.runningAgentCount ?? 0),
665
- 'CompanyCore.WorkItemCount': String(result?.workItemCount ?? 0),
666
- 'CompanyCore.ArtifactCount': String(result?.artifactCount ?? 0),
667
- 'CompanyCore.GrowthResultCount': String(result?.growthResultCount ?? 0),
668
- 'CompanyCore.RevenueResultCount': String(result?.revenueResultCount ?? 0),
669
- 'CompanyCore.LastSnapshotAt': result?.lastSnapshotAt || null,
670
- 'CompanyCore.LastMessage': result?.lastMessage || 'Company is running from this note in the background.'
671
- });
672
- }
673
-
674
- function clearCompanyBindingValues(nodeEntry) {
675
- if (!nodeEntry?.model) return;
676
-
677
- applyCompanyBindingValues(nodeEntry.model, {
678
- 'CompanyCore.CompanyId': null,
679
- 'CompanyCore.RunId': null,
680
- 'CompanyCore.CoreBaseUrl': null,
681
- 'CompanyCore.SourceNodeId': null,
682
- 'CompanyCore.BoardId': null,
683
- 'CompanyCore.ExecutionMode': null,
684
- 'CompanyCore.SubAgentsEnabled': null,
685
- 'CompanyCore.Status': null,
686
- 'CompanyCore.LastMessage': null,
687
- 'CompanyCore.PendingApprovalCount': null,
688
- 'CompanyCore.RunningAgentCount': null,
689
- 'CompanyCore.WorkItemCount': null,
690
- 'CompanyCore.ArtifactCount': null,
691
- 'CompanyCore.GrowthResultCount': null,
692
- 'CompanyCore.RevenueResultCount': null,
693
- 'CompanyCore.LastSnapshotAt': null
694
- });
695
- }
696
-
697
- function syncCompanyNodeVisual(nodeEntry) {
698
- if (!nodeEntry?.cssObject || !window.MindMapCss3DManager?.syncCss3dContent) {
699
- return;
700
- }
701
-
702
- nodeEntry.isCssDirty = true;
703
- window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
704
- nodeEntry.isCssDirty = false;
705
- _module?.lodRenderer?.requestResidentPatch?.('update-node', nodeEntry.model?.id || nodeEntry.model?.Id);
706
- }
707
-
708
- function canRunAsCompany(model) {
709
- if (!model) return false;
710
- if (isCompanyBoundNode(model)) return true;
711
-
712
- const contentType = getNodeContentType(model);
713
- if (!contentType) return true;
714
-
715
- if (['image', 'video', 'pdf', 'file', 'directory'].includes(contentType)) {
716
- return false;
717
- }
718
-
719
- if (['note', 'text', 'markdown', 'memo', 'code', 'mask'].includes(contentType)) {
720
- return true;
721
- }
722
-
723
- return getNodeSourceFilePath(model).length === 0;
724
- }
725
-
726
- function getMemoColorKey(model) {
727
- const metadata = ensureMetadata(model);
728
- const fallbackKey = isAgentStyledMemoNode(model) ? 'gray' : 'coral';
729
- const key = String(metadata.memoColor || fallbackKey).toLowerCase();
730
- return MEMO_COLORS.some(color => color.key === key) ? key : fallbackKey;
731
- }
732
-
733
- function buildMemoColorPopover(selectedKey) {
734
- const popover = document.createElement('div');
735
- popover.className = 'menu-color-popover';
736
-
737
- MEMO_COLORS.forEach(color => {
738
- const button = document.createElement('button');
739
- button.type = 'button';
740
- button.className = 'menu-color-swatch';
741
- button.dataset.colorKey = color.key;
742
- button.title = color.key;
743
- button.style.setProperty('--swatch-color', color.swatch);
744
- button.style.setProperty('--swatch-ring', color.ring);
745
- button.classList.toggle('is-active', color.key === selectedKey);
746
- popover.appendChild(button);
747
- });
748
-
749
- return popover;
750
- }
751
-
752
- function syncMemoColorPopover(popover, selectedKey) {
753
- if (!popover) return;
754
- popover.querySelectorAll('.menu-color-swatch').forEach(button => {
755
- button.classList.toggle('is-active', button.dataset.colorKey === selectedKey);
756
- });
757
- }
758
-
759
- function refreshMemoNode(nodeEntry) {
760
- if (!nodeEntry) return;
761
- nodeEntry.isCssDirty = false;
762
- if (nodeEntry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
763
- window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
764
- }
765
- if (_module?.lodRenderer?.requestResidentPatch) {
766
- _module.lodRenderer.requestResidentPatch('update-node', nodeEntry.model?.id || nodeEntry.model?.Id);
767
- }
768
- }
6
+ let currentMenuElement = null; // DOM overlay element
7
+ let currentMenuNodeId = null;
8
+ let _module = null;
9
+ const MEMO_COLORS = [
10
+ { key: 'coral', swatch: '#ff8052', ring: 'rgba(255, 128, 82, 0.22)' },
11
+ { key: 'amber', swatch: '#f59e0b', ring: 'rgba(245, 158, 11, 0.22)' },
12
+ { key: 'sun', swatch: '#ca8a04', ring: 'rgba(202, 138, 4, 0.22)' },
13
+ { key: 'sage', swatch: '#65a30d', ring: 'rgba(101, 163, 13, 0.22)' },
14
+ { key: 'mint', swatch: '#10b981', ring: 'rgba(16, 185, 129, 0.22)' },
15
+ { key: 'sky', swatch: '#3b82f6', ring: 'rgba(59, 130, 246, 0.22)' },
16
+ { key: 'indigo', swatch: '#6366f1', ring: 'rgba(99, 102, 241, 0.22)' },
17
+ { key: 'violet', swatch: '#8b5cf6', ring: 'rgba(139, 92, 246, 0.22)' },
18
+ { key: 'rose', swatch: '#f43f5e', ring: 'rgba(244, 63, 94, 0.22)' },
19
+ { key: 'gray', swatch: '#6b7280', ring: 'rgba(107, 114, 128, 0.22)' },
20
+ { key: 'slate', swatch: '#64748b', ring: 'rgba(100, 116, 139, 0.22)' }
21
+ ];
22
+ const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
23
+ const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
24
+
25
+ function ensureFeedbackStyles() {
26
+ if (document.getElementById('mindmap-feedback-styles')) return;
27
+
28
+ const style = document.createElement('style');
29
+ style.id = 'mindmap-feedback-styles';
30
+ style.textContent = `
31
+ .mindmap-toast-stack {
32
+ position: fixed;
33
+ right: 22px;
34
+ bottom: 24px;
35
+ z-index: 2147483000;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 10px;
39
+ pointer-events: none;
40
+ }
41
+
42
+ .mindmap-toast {
43
+ width: min(380px, calc(100vw - 32px));
44
+ padding: 12px 14px;
45
+ border-radius: 16px;
46
+ background: rgba(15, 23, 42, 0.94);
47
+ color: #f8fafc;
48
+ box-shadow: 0 18px 42px rgba(15, 23, 42, 0.24);
49
+ font: 600 13px/1.45 Inter, ui-sans-serif, system-ui, sans-serif;
50
+ opacity: 0;
51
+ transform: translateY(8px);
52
+ transition: opacity 180ms ease, transform 180ms ease;
53
+ pointer-events: auto;
54
+ }
55
+
56
+ .mindmap-toast.is-visible {
57
+ opacity: 1;
58
+ transform: translateY(0);
59
+ }
60
+
61
+ .mindmap-toast[data-kind="error"] {
62
+ background: rgba(127, 29, 29, 0.94);
63
+ }
64
+
65
+ .mindmap-confirm-backdrop {
66
+ position: fixed;
67
+ inset: 0;
68
+ z-index: 2147483001;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ padding: 24px;
73
+ background: rgba(15, 23, 42, 0.20);
74
+ backdrop-filter: blur(5px);
75
+ }
76
+
77
+ .mindmap-confirm-panel {
78
+ width: min(440px, calc(100vw - 36px));
79
+ border: 1px solid rgba(203, 213, 225, 0.86);
80
+ border-radius: 22px;
81
+ background: rgba(248, 250, 252, 0.98);
82
+ box-shadow: 0 28px 72px rgba(15, 23, 42, 0.22);
83
+ padding: 20px;
84
+ color: #0f172a;
85
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
86
+ }
87
+
88
+ .mindmap-confirm-title {
89
+ margin: 0;
90
+ font-size: 15px;
91
+ font-weight: 800;
92
+ letter-spacing: 0.01em;
93
+ }
94
+
95
+ .mindmap-confirm-message {
96
+ margin: 8px 0 0;
97
+ color: #475569;
98
+ font-size: 13px;
99
+ line-height: 1.55;
100
+ }
101
+
102
+ .mindmap-confirm-actions {
103
+ display: flex;
104
+ justify-content: flex-end;
105
+ gap: 10px;
106
+ margin-top: 18px;
107
+ }
108
+
109
+ .mindmap-confirm-button {
110
+ border: 0;
111
+ border-radius: 999px;
112
+ padding: 9px 15px;
113
+ font-size: 13px;
114
+ font-weight: 800;
115
+ cursor: pointer;
116
+ transition: transform 120ms ease, background 120ms ease;
117
+ }
118
+
119
+ .mindmap-confirm-button:active {
120
+ transform: scale(0.98);
121
+ }
122
+
123
+ .mindmap-confirm-button.secondary {
124
+ background: #e0f2fe;
125
+ color: #075985;
126
+ }
127
+
128
+ .mindmap-confirm-button.primary {
129
+ background: #0f172a;
130
+ color: #ffffff;
131
+ }
132
+ `;
133
+ document.head.appendChild(style);
134
+ }
135
+
136
+ function getToastStack() {
137
+ ensureFeedbackStyles();
138
+ let stack = document.querySelector('.mindmap-toast-stack');
139
+ if (!stack) {
140
+ stack = document.createElement('div');
141
+ stack.className = 'mindmap-toast-stack';
142
+ document.body.appendChild(stack);
143
+ }
144
+ return stack;
145
+ }
146
+
147
+ function showToast(message, options = {}) {
148
+ const text = String(message || '').trim();
149
+ if (!text || !document?.body) return;
150
+
151
+ const toast = document.createElement('div');
152
+ toast.className = 'mindmap-toast';
153
+ toast.dataset.kind = options.kind || 'info';
154
+ toast.textContent = text;
155
+
156
+ getToastStack().appendChild(toast);
157
+ requestAnimationFrame(() => toast.classList.add('is-visible'));
158
+
159
+ window.setTimeout(() => {
160
+ toast.classList.remove('is-visible');
161
+ window.setTimeout(() => toast.remove(), 220);
162
+ }, options.duration || 2600);
163
+ }
164
+
165
+ function showConfirmDialog(options = {}) {
166
+ if (!document?.body) return Promise.resolve(false);
167
+
168
+ ensureFeedbackStyles();
169
+
170
+ return new Promise((resolve) => {
171
+ const backdrop = document.createElement('div');
172
+ backdrop.className = 'mindmap-confirm-backdrop';
173
+
174
+ const panel = document.createElement('div');
175
+ panel.className = 'mindmap-confirm-panel';
176
+ panel.setAttribute('role', 'dialog');
177
+ panel.setAttribute('aria-modal', 'true');
178
+
179
+ const title = document.createElement('h3');
180
+ title.className = 'mindmap-confirm-title';
181
+ title.textContent = options.title || 'Confirm action';
182
+
183
+ const message = document.createElement('p');
184
+ message.className = 'mindmap-confirm-message';
185
+ message.textContent = options.message || 'Do you want to continue?';
186
+
187
+ const actions = document.createElement('div');
188
+ actions.className = 'mindmap-confirm-actions';
189
+
190
+ const cancel = document.createElement('button');
191
+ cancel.type = 'button';
192
+ cancel.className = 'mindmap-confirm-button secondary';
193
+ cancel.textContent = options.cancelText || 'Cancel';
194
+
195
+ const confirm = document.createElement('button');
196
+ confirm.type = 'button';
197
+ confirm.className = 'mindmap-confirm-button primary';
198
+ confirm.textContent = options.confirmText || 'Confirm';
199
+
200
+ const close = (value) => {
201
+ backdrop.remove();
202
+ document.removeEventListener('keydown', handleKeyDown, true);
203
+ resolve(!!value);
204
+ };
205
+
206
+ const handleKeyDown = (event) => {
207
+ if (event.key === 'Escape') {
208
+ event.preventDefault();
209
+ close(false);
210
+ }
211
+ };
212
+
213
+ cancel.addEventListener('click', () => close(false));
214
+ confirm.addEventListener('click', () => close(true));
215
+ backdrop.addEventListener('click', (event) => {
216
+ if (event.target === backdrop) close(false);
217
+ });
218
+ document.addEventListener('keydown', handleKeyDown, true);
219
+
220
+ actions.appendChild(cancel);
221
+ actions.appendChild(confirm);
222
+ panel.appendChild(title);
223
+ panel.appendChild(message);
224
+ panel.appendChild(actions);
225
+ backdrop.appendChild(panel);
226
+ document.body.appendChild(backdrop);
227
+ confirm.focus({ preventScroll: true });
228
+ });
229
+ }
230
+
231
+ window.MindMapFeedback = window.MindMapFeedback || {};
232
+ window.MindMapFeedback.toast = showToast;
233
+ window.MindMapFeedback.confirm = showConfirmDialog;
234
+
235
+ function init(module) {
236
+ _module = module;
237
+ }
238
+
239
+ function ensureMetadata(model) {
240
+ if (!model) return {};
241
+ if (model.metadata && typeof model.metadata === 'object') return model.metadata;
242
+ if (model.Metadata && typeof model.Metadata === 'object') {
243
+ model.metadata = model.Metadata;
244
+ return model.metadata;
245
+ }
246
+ model.metadata = {};
247
+ model.Metadata = model.metadata;
248
+ return model.metadata;
249
+ }
250
+
251
+ function getNodeSemanticType(model) {
252
+ const metadata = ensureMetadata(model);
253
+ return String(metadata.SemanticType || metadata.semanticType || '').trim();
254
+ }
255
+
256
+ function isAgentStyledMemoNode(model) {
257
+ const semanticType = getNodeSemanticType(model);
258
+ return semanticType === 'MindCanvasAgent'
259
+ || semanticType === 'AgentCommand'
260
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
261
+ }
262
+
263
+ function isBusinessAutomationNode(model) {
264
+ return getNodeSemanticType(model) === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
265
+ }
266
+
267
+ function getBusinessAutomationNodeKind(model) {
268
+ const metadata = ensureMetadata(model);
269
+ return String(metadata.AutomationNodeKind || metadata.automationNodeKind || '').trim().toLowerCase();
270
+ }
271
+
272
+ function getBusinessAutomationNodeRole(model) {
273
+ const metadata = ensureMetadata(model);
274
+ return String(metadata.AutomationNodeRole || metadata.automationNodeRole || '').trim().toLowerCase();
275
+ }
276
+
277
+ function isBusinessAutomationRootAiTaskNode(model) {
278
+ if (!isBusinessAutomationNode(model) || getBusinessAutomationNodeKind(model) !== 'ai-task') {
279
+ return false;
280
+ }
281
+
282
+ const role = getBusinessAutomationNodeRole(model);
283
+ return role.length === 0 || role === 'root';
284
+ }
285
+
286
+ function isRemoteFleetMonitorNode(model) {
287
+ return getNodeSemanticType(model) === REMOTE_FLEET_SEMANTIC_TYPE;
288
+ }
289
+
290
+ function canRunExecutableNode(model) {
291
+ return isBusinessAutomationRootAiTaskNode(model);
292
+ }
293
+
294
+ function isExecutableNodeRunning(model) {
295
+ const metadata = ensureMetadata(model);
296
+ return String(metadata.AutomationRunState || '').trim().toLowerCase() === 'running';
297
+ }
298
+
299
+ function setExecutableNodeRunVisualState(nodeId, runState, statusLabel) {
300
+ const normalizedNodeId = String(nodeId || '').trim();
301
+ if (!normalizedNodeId || !_module?.nodeObjectsById) {
302
+ return false;
303
+ }
304
+
305
+ const entry = _module.nodeObjectsById.get(normalizedNodeId);
306
+ if (!entry?.model) {
307
+ return false;
308
+ }
309
+
310
+ const metadata = ensureMetadata(entry.model);
311
+ const normalizedRunState = String(runState || '').trim().toLowerCase();
312
+ metadata.AutomationRunState = normalizedRunState || 'idle';
313
+
314
+ if (normalizedRunState === 'running') {
315
+ metadata.AutomationLastRunStatus = 'running';
316
+ metadata.AutomationLastRunStatusLabel = statusLabel || 'Running';
317
+ metadata.AutomationLastRunError = '';
318
+ } else if (statusLabel) {
319
+ metadata.AutomationLastRunStatusLabel = statusLabel;
320
+ }
321
+
322
+ entry.model.metadata = metadata;
323
+ entry.model.Metadata = metadata;
324
+
325
+ if (entry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
326
+ try {
327
+ entry.isCssDirty = true;
328
+ window.MindMapCss3DManager.syncCss3dContent(entry.model, entry.cssObject);
329
+ } finally {
330
+ entry.isCssDirty = false;
331
+ }
332
+ }
333
+
334
+ _module.lodRenderer?.requestVisualRefresh?.('node-run-visual-state');
335
+ return true;
336
+ }
337
+
338
+ async function syncNodeRunResultStates(result) {
339
+ if (!result) {
340
+ return;
341
+ }
342
+
343
+ const nodeStates = Array.isArray(result.nodeStates)
344
+ ? result.nodeStates
345
+ : (result.agentNodeState ? [result.agentNodeState] : []);
346
+
347
+ if (nodeStates.length > 0 && window.mindMap?.syncNodeStates) {
348
+ await window.mindMap.syncNodeStates(nodeStates);
349
+ }
350
+ }
351
+
352
+ function isTextFitNode(model) {
353
+ const contentType = String(model?.contentType || '').toLowerCase();
354
+ return contentType === 'text' || contentType === 'markdown' || contentType === 'note' || contentType === 'code';
355
+ }
356
+
357
+ function getNodeContentType(model) {
358
+ return String(model?.contentType || '').trim().toLowerCase();
359
+ }
360
+
361
+ function showActionSuccessIcon(button) {
362
+ const icon = button?.querySelector?.('i');
363
+ if (!icon) return;
364
+
365
+ const oldClass = icon.className;
366
+ icon.className = 'fa-solid fa-check text-green-500';
367
+ setTimeout(() => {
368
+ icon.className = oldClass;
369
+ }, 1000);
370
+ }
371
+
372
+ function getImageClipboardSourceCandidates(nodeModel) {
373
+ const assetUrls = window.MindMapNodes?.getNodeImageAssetUrls?.(nodeModel) || {};
374
+ const rawCandidates = [
375
+ assetUrls.fullResUrl,
376
+ assetUrls.originalUrl,
377
+ assetUrls.originalPath,
378
+ assetUrls.previewUrl,
379
+ assetUrls.responseUrl,
380
+ nodeModel?.response,
381
+ nodeModel?.Response
382
+ ];
383
+
384
+ const candidates = [];
385
+ rawCandidates.forEach(value => {
386
+ const normalized = typeof value === 'string' ? value.trim() : '';
387
+ if (!normalized || candidates.includes(normalized)) {
388
+ return;
389
+ }
390
+
391
+ candidates.push(normalized);
392
+ });
393
+
394
+ return candidates;
395
+ }
396
+
397
+ function shouldResolveImageClipboardSourceBeforeFetch(src) {
398
+ const normalized = typeof src === 'string' ? src.trim().toLowerCase() : '';
399
+ if (!normalized || normalized.startsWith('blob:') || normalized.startsWith('data:') || normalized.startsWith('file:')) {
400
+ return false;
401
+ }
402
+
403
+ return normalized.startsWith('/assets/')
404
+ || normalized.startsWith('assets/')
405
+ || normalized.includes('/assets/thumbs/')
406
+ || (normalized.includes('/assets/') && !/\.[a-z0-9]{2,5}(?:[?#]|$)/i.test(normalized));
407
+ }
408
+
409
+ async function resolveClipboardImageSource(nodeModel, src) {
410
+ if (!shouldResolveImageClipboardSourceBeforeFetch(src) || !_module?.dotNetHelper) {
411
+ return src;
412
+ }
413
+
414
+ const nodeId = String(nodeModel?.id || nodeModel?.Id || '').trim();
415
+ if (!nodeId) {
416
+ return src;
417
+ }
418
+
419
+ try {
420
+ const resolved = await _module.dotNetHelper.invokeMethodAsync('ResolveNodeAssetUrl', nodeId);
421
+ const normalized = typeof resolved === 'string' ? resolved.trim() : '';
422
+ return normalized || src;
423
+ } catch (error) {
424
+ console.warn('[MindMapMenuManager] Failed to resolve image clipboard source.', error);
425
+ return src;
426
+ }
427
+ }
428
+
429
+ async function fetchClipboardImageBlob(nodeModel) {
430
+ const candidates = getImageClipboardSourceCandidates(nodeModel);
431
+ let lastError = null;
432
+
433
+ for (const src of candidates) {
434
+ try {
435
+ const resolvedSrc = await resolveClipboardImageSource(nodeModel, src);
436
+ const response = await fetch(resolvedSrc, { cache: 'no-store' });
437
+ if (!response.ok) {
438
+ throw new Error(`HTTP ${response.status}`);
439
+ }
440
+
441
+ const blob = await response.blob();
442
+ if (!(blob instanceof Blob) || blob.size <= 0) {
443
+ throw new Error('Empty image blob.');
444
+ }
445
+
446
+ return { blob, src: resolvedSrc };
447
+ } catch (error) {
448
+ lastError = error;
449
+ console.warn(`[MindMapMenuManager] Failed to fetch clipboard image from ${src}`, error);
450
+ }
451
+ }
452
+
453
+ throw lastError || new Error('No image source was available for clipboard copy.');
454
+ }
455
+
456
+ async function decodeClipboardImageBlob(blob) {
457
+ if (typeof createImageBitmap === 'function') {
458
+ try {
459
+ const bitmap = await createImageBitmap(blob);
460
+ return {
461
+ width: bitmap.width,
462
+ height: bitmap.height,
463
+ draw(ctx, width, height) {
464
+ ctx.drawImage(bitmap, 0, 0, width, height);
465
+ },
466
+ dispose() {
467
+ bitmap.close?.();
468
+ }
469
+ };
470
+ } catch (error) {
471
+ console.warn('[MindMapMenuManager] createImageBitmap failed for clipboard image conversion.', error);
472
+ }
473
+ }
474
+
475
+ const objectUrl = URL.createObjectURL(blob);
476
+ try {
477
+ const image = await new Promise((resolve, reject) => {
478
+ const img = new Image();
479
+ img.decoding = 'async';
480
+ img.onload = () => resolve(img);
481
+ img.onerror = () => reject(new Error('Failed to decode image blob.'));
482
+ img.src = objectUrl;
483
+ });
484
+
485
+ return {
486
+ width: image.naturalWidth || image.width,
487
+ height: image.naturalHeight || image.height,
488
+ draw(ctx, width, height) {
489
+ ctx.drawImage(image, 0, 0, width, height);
490
+ },
491
+ dispose() {
492
+ image.remove?.();
493
+ URL.revokeObjectURL(objectUrl);
494
+ }
495
+ };
496
+ } catch (error) {
497
+ URL.revokeObjectURL(objectUrl);
498
+ throw error;
499
+ }
500
+ }
501
+
502
+ async function convertBlobToPngForClipboard(blob) {
503
+ const normalizedType = String(blob?.type || '').toLowerCase();
504
+ if (normalizedType === 'image/png' && blob.size > 0) {
505
+ return blob;
506
+ }
507
+
508
+ const decoded = await decodeClipboardImageBlob(blob);
509
+ const width = Math.max(1, Math.round(decoded?.width || 0));
510
+ const height = Math.max(1, Math.round(decoded?.height || 0));
511
+
512
+ if (!width || !height) {
513
+ decoded?.dispose?.();
514
+ throw new Error('Decoded image had no dimensions.');
515
+ }
516
+
517
+ try {
518
+ if (typeof OffscreenCanvas === 'function') {
519
+ const canvas = new OffscreenCanvas(width, height);
520
+ const ctx = canvas.getContext('2d');
521
+ if (!ctx) {
522
+ throw new Error('OffscreenCanvas 2D context is unavailable.');
523
+ }
524
+
525
+ decoded.draw(ctx, width, height);
526
+ return await canvas.convertToBlob({ type: 'image/png' });
527
+ }
528
+
529
+ const canvas = document.createElement('canvas');
530
+ canvas.width = width;
531
+ canvas.height = height;
532
+ const ctx = canvas.getContext('2d');
533
+ if (!ctx) {
534
+ throw new Error('Canvas 2D context is unavailable.');
535
+ }
536
+
537
+ decoded.draw(ctx, width, height);
538
+ return await new Promise((resolve, reject) => {
539
+ canvas.toBlob(result => {
540
+ if (result instanceof Blob && result.size > 0) {
541
+ resolve(result);
542
+ return;
543
+ }
544
+
545
+ reject(new Error('Failed to encode clipboard image as PNG.'));
546
+ }, 'image/png');
547
+ });
548
+ } finally {
549
+ decoded?.dispose?.();
550
+ }
551
+ }
552
+
553
+ async function writeImageBlobToClipboard(blob, fallbackText) {
554
+ if (navigator.clipboard?.write && typeof ClipboardItem === 'function') {
555
+ await navigator.clipboard.write([
556
+ new ClipboardItem({ 'image/png': blob })
557
+ ]);
558
+ return 'image';
559
+ }
560
+
561
+ if (navigator.clipboard?.writeText && fallbackText) {
562
+ await navigator.clipboard.writeText(fallbackText);
563
+ return 'text';
564
+ }
565
+
566
+ throw new Error('Clipboard API is unavailable.');
567
+ }
568
+
569
+ function getNodeSourceFilePath(model) {
570
+ return String(model?.sourceFilePath || model?.SourceFilePath || '').trim();
571
+ }
572
+
573
+ function getCompanyBindingValue(model, key) {
574
+ const metadata = ensureMetadata(model);
575
+ return String(metadata[key] || '').trim();
576
+ }
577
+
578
+ function isCompanyBoundNode(model) {
579
+ return getCompanyBindingValue(model, 'CompanyCore.CompanyId').length > 0;
580
+ }
581
+
582
+ function getCompanyStatus(model) {
583
+ return getCompanyBindingValue(model, 'CompanyCore.Status').toLowerCase();
584
+ }
585
+
586
+ function isPendingCompanyBinding(model) {
587
+ const companyId = getCompanyBindingValue(model, 'CompanyCore.CompanyId');
588
+ if (companyId.startsWith('pending:')) {
589
+ return true;
590
+ }
591
+
592
+ const status = getCompanyStatus(model);
593
+ const runId = getCompanyBindingValue(model, 'CompanyCore.RunId');
594
+ return !companyId && status === 'queued' && !runId;
595
+ }
596
+
597
+ function applyCompanyBindingValues(model, values) {
598
+ const metadata = ensureMetadata(model);
599
+ Object.entries(values || {}).forEach(([key, value]) => {
600
+ if (value === null || value === undefined || value === '') {
601
+ delete metadata[key];
602
+ return;
603
+ }
604
+
605
+ metadata[key] = String(value);
606
+ });
607
+ model.metadata = metadata;
608
+ model.Metadata = metadata;
609
+ }
610
+
611
+ function applyOptimisticCompanyRunState(nodeEntry, nodeId, executionMode, enableSubAgents) {
612
+ if (!nodeEntry?.model) return;
613
+
614
+ applyCompanyBindingValues(nodeEntry.model, {
615
+ 'CompanyCore.CompanyId': `pending:${nodeId}`,
616
+ 'CompanyCore.ExecutionMode': executionMode || 'Auto',
617
+ 'CompanyCore.SubAgentsEnabled': enableSubAgents ? 'true' : 'false',
618
+ 'CompanyCore.Status': 'Queued',
619
+ 'CompanyCore.LastMessage': 'Starting company from this note...',
620
+ 'CompanyCore.PendingApprovalCount': '0',
621
+ 'CompanyCore.RunningAgentCount': '0',
622
+ 'CompanyCore.WorkItemCount': '0',
623
+ 'CompanyCore.ArtifactCount': '0',
624
+ 'CompanyCore.GrowthResultCount': '0',
625
+ 'CompanyCore.RevenueResultCount': '0'
626
+ });
627
+ }
628
+
629
+ function applyCompanyRunResult(nodeEntry, result, fallbackExecutionMode, fallbackEnableSubAgents) {
630
+ if (!nodeEntry?.model) return;
631
+
632
+ const companyId = String(result?.companyId || '').trim();
633
+ const status = String(result?.status || '').trim();
634
+ if (!companyId) {
635
+ applyCompanyBindingValues(nodeEntry.model, {
636
+ 'CompanyCore.CompanyId': null,
637
+ 'CompanyCore.RunId': null,
638
+ 'CompanyCore.ExecutionMode': result?.executionMode || fallbackExecutionMode || 'Auto',
639
+ 'CompanyCore.SubAgentsEnabled': typeof result?.subAgentsEnabled === 'boolean'
640
+ ? (result.subAgentsEnabled ? 'true' : 'false')
641
+ : (fallbackEnableSubAgents ? 'true' : 'false'),
642
+ 'CompanyCore.Status': status || 'Failed',
643
+ 'CompanyCore.LastMessage': result?.lastMessage || 'Company did not start from this note.',
644
+ 'CompanyCore.PendingApprovalCount': String(result?.pendingApprovalCount ?? 0),
645
+ 'CompanyCore.RunningAgentCount': String(result?.runningAgentCount ?? 0),
646
+ 'CompanyCore.WorkItemCount': String(result?.workItemCount ?? 0),
647
+ 'CompanyCore.ArtifactCount': String(result?.artifactCount ?? 0),
648
+ 'CompanyCore.GrowthResultCount': String(result?.growthResultCount ?? 0),
649
+ 'CompanyCore.RevenueResultCount': String(result?.revenueResultCount ?? 0),
650
+ 'CompanyCore.LastSnapshotAt': result?.lastSnapshotAt || null
651
+ });
652
+ return;
653
+ }
654
+
655
+ applyCompanyBindingValues(nodeEntry.model, {
656
+ 'CompanyCore.CompanyId': companyId,
657
+ 'CompanyCore.RunId': result?.runId || null,
658
+ 'CompanyCore.ExecutionMode': result?.executionMode || fallbackExecutionMode || 'Auto',
659
+ 'CompanyCore.SubAgentsEnabled': typeof result?.subAgentsEnabled === 'boolean'
660
+ ? (result.subAgentsEnabled ? 'true' : 'false')
661
+ : (fallbackEnableSubAgents ? 'true' : 'false'),
662
+ 'CompanyCore.Status': status || 'Queued',
663
+ 'CompanyCore.PendingApprovalCount': String(result?.pendingApprovalCount ?? 0),
664
+ 'CompanyCore.RunningAgentCount': String(result?.runningAgentCount ?? 0),
665
+ 'CompanyCore.WorkItemCount': String(result?.workItemCount ?? 0),
666
+ 'CompanyCore.ArtifactCount': String(result?.artifactCount ?? 0),
667
+ 'CompanyCore.GrowthResultCount': String(result?.growthResultCount ?? 0),
668
+ 'CompanyCore.RevenueResultCount': String(result?.revenueResultCount ?? 0),
669
+ 'CompanyCore.LastSnapshotAt': result?.lastSnapshotAt || null,
670
+ 'CompanyCore.LastMessage': result?.lastMessage || 'Company is running from this note in the background.'
671
+ });
672
+ }
673
+
674
+ function clearCompanyBindingValues(nodeEntry) {
675
+ if (!nodeEntry?.model) return;
676
+
677
+ applyCompanyBindingValues(nodeEntry.model, {
678
+ 'CompanyCore.CompanyId': null,
679
+ 'CompanyCore.RunId': null,
680
+ 'CompanyCore.CoreBaseUrl': null,
681
+ 'CompanyCore.SourceNodeId': null,
682
+ 'CompanyCore.BoardId': null,
683
+ 'CompanyCore.ExecutionMode': null,
684
+ 'CompanyCore.SubAgentsEnabled': null,
685
+ 'CompanyCore.Status': null,
686
+ 'CompanyCore.LastMessage': null,
687
+ 'CompanyCore.PendingApprovalCount': null,
688
+ 'CompanyCore.RunningAgentCount': null,
689
+ 'CompanyCore.WorkItemCount': null,
690
+ 'CompanyCore.ArtifactCount': null,
691
+ 'CompanyCore.GrowthResultCount': null,
692
+ 'CompanyCore.RevenueResultCount': null,
693
+ 'CompanyCore.LastSnapshotAt': null
694
+ });
695
+ }
696
+
697
+ function syncCompanyNodeVisual(nodeEntry) {
698
+ if (!nodeEntry?.cssObject || !window.MindMapCss3DManager?.syncCss3dContent) {
699
+ return;
700
+ }
701
+
702
+ nodeEntry.isCssDirty = true;
703
+ window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
704
+ nodeEntry.isCssDirty = false;
705
+ _module?.lodRenderer?.requestResidentPatch?.('update-node', nodeEntry.model?.id || nodeEntry.model?.Id);
706
+ }
707
+
708
+ function canRunAsCompany(model) {
709
+ if (!model) return false;
710
+ if (isCompanyBoundNode(model)) return true;
711
+
712
+ const contentType = getNodeContentType(model);
713
+ if (!contentType) return true;
714
+
715
+ if (['image', 'video', 'pdf', 'file', 'directory'].includes(contentType)) {
716
+ return false;
717
+ }
718
+
719
+ if (['note', 'text', 'markdown', 'memo', 'code', 'mask'].includes(contentType)) {
720
+ return true;
721
+ }
722
+
723
+ return getNodeSourceFilePath(model).length === 0;
724
+ }
725
+
726
+ function getMemoColorKey(model) {
727
+ const metadata = ensureMetadata(model);
728
+ const fallbackKey = isAgentStyledMemoNode(model) ? 'gray' : 'coral';
729
+ const key = String(metadata.memoColor || fallbackKey).toLowerCase();
730
+ return MEMO_COLORS.some(color => color.key === key) ? key : fallbackKey;
731
+ }
732
+
733
+ function buildMemoColorPopover(selectedKey) {
734
+ const popover = document.createElement('div');
735
+ popover.className = 'menu-color-popover';
736
+
737
+ MEMO_COLORS.forEach(color => {
738
+ const button = document.createElement('button');
739
+ button.type = 'button';
740
+ button.className = 'menu-color-swatch';
741
+ button.dataset.colorKey = color.key;
742
+ button.title = color.key;
743
+ button.style.setProperty('--swatch-color', color.swatch);
744
+ button.style.setProperty('--swatch-ring', color.ring);
745
+ button.classList.toggle('is-active', color.key === selectedKey);
746
+ popover.appendChild(button);
747
+ });
748
+
749
+ return popover;
750
+ }
751
+
752
+ function syncMemoColorPopover(popover, selectedKey) {
753
+ if (!popover) return;
754
+ popover.querySelectorAll('.menu-color-swatch').forEach(button => {
755
+ button.classList.toggle('is-active', button.dataset.colorKey === selectedKey);
756
+ });
757
+ }
758
+
759
+ function refreshMemoNode(nodeEntry) {
760
+ if (!nodeEntry) return;
761
+ nodeEntry.isCssDirty = false;
762
+ if (nodeEntry.cssObject && window.MindMapCss3DManager?.syncCss3dContent) {
763
+ window.MindMapCss3DManager.syncCss3dContent(nodeEntry.model, nodeEntry.cssObject);
764
+ }
765
+ if (_module?.lodRenderer?.requestResidentPatch) {
766
+ _module.lodRenderer.requestResidentPatch('update-node', nodeEntry.model?.id || nodeEntry.model?.Id);
767
+ }
768
+ }
769
769
 
770
770
  // ▼▼▼ [Refactor] Screen-space DOM overlay instead of CSS3DObject ▼▼▼
771
771
  function showMenu(nodeId) {
@@ -842,25 +842,25 @@
842
842
  }
843
843
 
844
844
  // Menu styling — screen-space overlay
845
- Object.assign(menuClone.style, {
846
- position: 'fixed',
847
- pointerEvents: 'auto',
848
- whiteSpace: 'nowrap',
849
- zIndex: '10000',
850
- transform: 'translate(-50%, -100%)',
851
- overflow: 'visible'
852
- });
845
+ Object.assign(menuClone.style, {
846
+ position: 'fixed',
847
+ pointerEvents: 'auto',
848
+ whiteSpace: 'nowrap',
849
+ zIndex: '10000',
850
+ transform: 'translate(-50%, -100%)',
851
+ overflow: 'visible'
852
+ });
853
853
 
854
854
  // Stop event bubbling
855
- const stopProp = (e) => {
856
- if (e.button === 1 || e.button === 2) {
857
- return;
858
- }
859
- e.stopPropagation();
860
- };
861
- menuClone.addEventListener('mousedown', stopProp);
862
- menuClone.addEventListener('mouseup', stopProp);
863
- menuClone.addEventListener('click', stopProp);
855
+ const stopProp = (e) => {
856
+ if (e.button === 1 || e.button === 2) {
857
+ return;
858
+ }
859
+ e.stopPropagation();
860
+ };
861
+ menuClone.addEventListener('mousedown', stopProp);
862
+ menuClone.addEventListener('mouseup', stopProp);
863
+ menuClone.addEventListener('click', stopProp);
864
864
  menuClone.addEventListener('dblclick', stopProp);
865
865
 
866
866
  // Bind button actions
@@ -869,29 +869,29 @@
869
869
  // Append to body
870
870
  document.body.appendChild(menuClone);
871
871
 
872
- // Position on screen
873
- updateMenuScreenPosition(menuClone, nodeEntry);
874
- requestAnimationFrame(() => {
875
- if (currentMenuElement === menuClone && currentMenuNodeId === nodeId) {
876
- updateMenuScreenPosition(menuClone, nodeEntry);
877
- }
878
- });
879
-
880
- currentMenuElement = menuClone;
881
- currentMenuNodeId = nodeId;
882
- }
872
+ // Position on screen
873
+ updateMenuScreenPosition(menuClone, nodeEntry);
874
+ requestAnimationFrame(() => {
875
+ if (currentMenuElement === menuClone && currentMenuNodeId === nodeId) {
876
+ updateMenuScreenPosition(menuClone, nodeEntry);
877
+ }
878
+ });
879
+
880
+ currentMenuElement = menuClone;
881
+ currentMenuNodeId = nodeId;
882
+ }
883
883
  // ▲▲▲ [Refactor] ▲▲▲
884
884
 
885
- function createMenuContainer(nodeEntry) {
886
- const contentType = nodeEntry.model.contentType;
887
- const prompt = nodeEntry.model.prompt || '';
888
- const isAiEnabled = nodeEntry.model.isAiEnabled !== false;
889
- const isWordWrap = nodeEntry.model.isWordWrap === true;
890
- const companyBound = isCompanyBoundNode(nodeEntry.model);
891
- const companyStatus = getCompanyStatus(nodeEntry.model);
892
-
893
- const container = document.createElement('div');
894
- container.className = 'node-menu-container';
885
+ function createMenuContainer(nodeEntry) {
886
+ const contentType = nodeEntry.model.contentType;
887
+ const prompt = nodeEntry.model.prompt || '';
888
+ const isAiEnabled = nodeEntry.model.isAiEnabled !== false;
889
+ const isWordWrap = nodeEntry.model.isWordWrap === true;
890
+ const companyBound = isCompanyBoundNode(nodeEntry.model);
891
+ const companyStatus = getCompanyStatus(nodeEntry.model);
892
+
893
+ const container = document.createElement('div');
894
+ container.className = 'node-menu-container';
895
895
 
896
896
  const addSeparator = () => {
897
897
  const sep = document.createElement('div');
@@ -899,134 +899,134 @@
899
899
  container.appendChild(sep);
900
900
  };
901
901
 
902
- const addButton = (className, title, iconClass, isDisabled = false) => {
903
- const btn = document.createElement('button');
904
- btn.className = `menu-btn ${className}${isDisabled ? ' disabled' : ''}`.trim();
905
- btn.title = title;
906
- btn.disabled = isDisabled === true;
907
- const icon = document.createElement('i');
908
- icon.className = iconClass;
909
- btn.appendChild(icon);
910
- return btn;
911
- };
912
-
913
- if (isRemoteFleetMonitorNode(nodeEntry.model)) {
914
- container.appendChild(addButton('delete-btn js-btn-delete', 'Delete', 'fa-solid fa-trash'));
915
- return container;
916
- }
917
-
918
- const appendNodeRunButton = () => {
919
- if (!canRunExecutableNode(nodeEntry.model)) {
920
- return false;
921
- }
922
-
923
- const isRunning = isExecutableNodeRunning(nodeEntry.model);
924
- container.appendChild(addButton(
925
- 'js-btn-node-run',
926
- isRunning ? 'Node is running' : 'Run node',
927
- isRunning ? 'fa-solid fa-spinner' : 'fa-solid fa-play',
928
- isRunning));
929
- return true;
930
- };
931
-
932
- const appendCompanyButtons = () => {
933
- if (!canRunAsCompany(nodeEntry.model)) {
934
- return false;
935
- }
936
-
937
- if (companyBound) {
938
- if (isPendingCompanyBinding(nodeEntry.model)) {
939
- container.appendChild(addButton('js-btn-company-pending', 'Company is starting', 'fa-solid fa-spinner', true));
940
- container.appendChild(addButton('js-btn-company-refresh', 'Refresh Company status', 'fa-solid fa-rotate'));
941
- container.appendChild(addButton('js-btn-company-remove', 'Remove Company from this note', 'fa-solid fa-xmark'));
942
- return true;
943
- }
944
-
945
- if (companyStatus !== 'archived') {
946
- container.appendChild(addButton('js-btn-company-sync-goal', 'Update Company goal from this note', 'fa-solid fa-bullseye'));
947
- }
948
- container.appendChild(addButton('js-btn-company-graph', 'Open Company graph', 'fa-solid fa-diagram-project'));
949
- container.appendChild(addButton('js-btn-company-refresh', 'Refresh Company status', 'fa-solid fa-rotate'));
950
-
951
- if (companyStatus === 'archived') {
952
- container.appendChild(addButton('js-btn-company-archived', 'Company is archived', 'fa-solid fa-box-archive', true));
953
- } else if (companyStatus === 'paused') {
954
- container.appendChild(addButton('js-btn-company-resume', 'Resume Company', 'fa-solid fa-play'));
955
- } else {
956
- container.appendChild(addButton('js-btn-company-pause', 'Pause Company', 'fa-solid fa-pause'));
957
- }
958
-
959
- if (companyStatus !== 'archived') {
960
- container.appendChild(addButton('js-btn-company-archive', 'Archive Company', 'fa-solid fa-box-archive'));
961
- }
962
-
963
- container.appendChild(addButton('js-btn-company-remove', 'Remove Company from this note', 'fa-solid fa-xmark'));
964
- return true;
965
- }
966
-
967
- return false;
968
- };
969
-
970
- // AI toggle (non-image)
971
- let hasPrimaryActions = false;
972
- if (contentType !== 'image' && contentType !== 'memo') {
973
- const aiBtn = addButton(`js-btn-ai ${isAiEnabled ? 'active-ai' : 'inactive-ai'}`, isAiEnabled ? 'Disable AI reference' : 'Enable AI reference', 'fa-solid fa-brain');
974
- if (!isAiEnabled) {
975
- const span = document.createElement('span');
976
- span.className = 'slash-overlay';
977
- span.textContent = '\\';
978
- aiBtn.appendChild(span);
979
- }
980
- container.appendChild(aiBtn);
981
- hasPrimaryActions = true;
982
- }
983
-
984
- hasPrimaryActions = appendNodeRunButton() || appendCompanyButtons() || hasPrimaryActions;
985
-
986
- if (hasPrimaryActions) {
987
- addSeparator();
988
- }
989
-
990
- // Type-specific buttons
991
- switch (contentType) {
992
- case 'text': {
993
- if (!nodeEntry.model.sourceFilePath) {
994
- container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
995
- }
996
- container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
997
- container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
998
- container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
999
- break;
1000
- }
1001
- case 'note': {
1002
- container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
1003
- container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1004
- container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1005
- container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1006
- break;
1007
- }
1008
- case 'memo': {
1009
- const colorBtn = addButton('js-btn-color', 'Change background color', 'fa-solid fa-palette');
1010
- colorBtn.appendChild(buildMemoColorPopover(getMemoColorKey(nodeEntry.model)));
1011
- container.appendChild(colorBtn);
1012
- container.appendChild(addButton('js-btn-copy', 'Copy memo', 'fa-regular fa-copy'));
1013
- break;
1014
- }
1015
- case 'code': {
1016
- const wrapBtn = addButton(`js-btn-wordwrap ${isWordWrap ? '' : 'active-nowrap'}`, isWordWrap ? 'Disable word wrap' : 'Enable word wrap', isWordWrap ? 'fa-solid fa-align-left' : 'fa-solid fa-arrows-left-right');
1017
- container.appendChild(wrapBtn);
1018
- container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
1019
- container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1020
- container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1021
- container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1022
- break;
1023
- }
1024
- case 'markdown': {
1025
- container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1026
- container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1027
- container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1028
- break;
1029
- }
902
+ const addButton = (className, title, iconClass, isDisabled = false) => {
903
+ const btn = document.createElement('button');
904
+ btn.className = `menu-btn ${className}${isDisabled ? ' disabled' : ''}`.trim();
905
+ btn.title = title;
906
+ btn.disabled = isDisabled === true;
907
+ const icon = document.createElement('i');
908
+ icon.className = iconClass;
909
+ btn.appendChild(icon);
910
+ return btn;
911
+ };
912
+
913
+ if (isRemoteFleetMonitorNode(nodeEntry.model)) {
914
+ container.appendChild(addButton('delete-btn js-btn-delete', 'Delete', 'fa-solid fa-trash'));
915
+ return container;
916
+ }
917
+
918
+ const appendNodeRunButton = () => {
919
+ if (!canRunExecutableNode(nodeEntry.model)) {
920
+ return false;
921
+ }
922
+
923
+ const isRunning = isExecutableNodeRunning(nodeEntry.model);
924
+ container.appendChild(addButton(
925
+ 'js-btn-node-run',
926
+ isRunning ? 'Node is running' : 'Run node',
927
+ isRunning ? 'fa-solid fa-spinner' : 'fa-solid fa-play',
928
+ isRunning));
929
+ return true;
930
+ };
931
+
932
+ const appendCompanyButtons = () => {
933
+ if (!canRunAsCompany(nodeEntry.model)) {
934
+ return false;
935
+ }
936
+
937
+ if (companyBound) {
938
+ if (isPendingCompanyBinding(nodeEntry.model)) {
939
+ container.appendChild(addButton('js-btn-company-pending', 'Company is starting', 'fa-solid fa-spinner', true));
940
+ container.appendChild(addButton('js-btn-company-refresh', 'Refresh Company status', 'fa-solid fa-rotate'));
941
+ container.appendChild(addButton('js-btn-company-remove', 'Remove Company from this note', 'fa-solid fa-xmark'));
942
+ return true;
943
+ }
944
+
945
+ if (companyStatus !== 'archived') {
946
+ container.appendChild(addButton('js-btn-company-sync-goal', 'Update Company goal from this note', 'fa-solid fa-bullseye'));
947
+ }
948
+ container.appendChild(addButton('js-btn-company-graph', 'Open Company graph', 'fa-solid fa-diagram-project'));
949
+ container.appendChild(addButton('js-btn-company-refresh', 'Refresh Company status', 'fa-solid fa-rotate'));
950
+
951
+ if (companyStatus === 'archived') {
952
+ container.appendChild(addButton('js-btn-company-archived', 'Company is archived', 'fa-solid fa-box-archive', true));
953
+ } else if (companyStatus === 'paused') {
954
+ container.appendChild(addButton('js-btn-company-resume', 'Resume Company', 'fa-solid fa-play'));
955
+ } else {
956
+ container.appendChild(addButton('js-btn-company-pause', 'Pause Company', 'fa-solid fa-pause'));
957
+ }
958
+
959
+ if (companyStatus !== 'archived') {
960
+ container.appendChild(addButton('js-btn-company-archive', 'Archive Company', 'fa-solid fa-box-archive'));
961
+ }
962
+
963
+ container.appendChild(addButton('js-btn-company-remove', 'Remove Company from this note', 'fa-solid fa-xmark'));
964
+ return true;
965
+ }
966
+
967
+ return false;
968
+ };
969
+
970
+ // AI toggle (non-image)
971
+ let hasPrimaryActions = false;
972
+ if (contentType !== 'image' && contentType !== 'memo') {
973
+ const aiBtn = addButton(`js-btn-ai ${isAiEnabled ? 'active-ai' : 'inactive-ai'}`, isAiEnabled ? 'Disable AI reference' : 'Enable AI reference', 'fa-solid fa-brain');
974
+ if (!isAiEnabled) {
975
+ const span = document.createElement('span');
976
+ span.className = 'slash-overlay';
977
+ span.textContent = '\\';
978
+ aiBtn.appendChild(span);
979
+ }
980
+ container.appendChild(aiBtn);
981
+ hasPrimaryActions = true;
982
+ }
983
+
984
+ hasPrimaryActions = appendNodeRunButton() || appendCompanyButtons() || hasPrimaryActions;
985
+
986
+ if (hasPrimaryActions) {
987
+ addSeparator();
988
+ }
989
+
990
+ // Type-specific buttons
991
+ switch (contentType) {
992
+ case 'text': {
993
+ if (!nodeEntry.model.sourceFilePath) {
994
+ container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
995
+ }
996
+ container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
997
+ container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
998
+ container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
999
+ break;
1000
+ }
1001
+ case 'note': {
1002
+ container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
1003
+ container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1004
+ container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1005
+ container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1006
+ break;
1007
+ }
1008
+ case 'memo': {
1009
+ const colorBtn = addButton('js-btn-color', 'Change background color', 'fa-solid fa-palette');
1010
+ colorBtn.appendChild(buildMemoColorPopover(getMemoColorKey(nodeEntry.model)));
1011
+ container.appendChild(colorBtn);
1012
+ container.appendChild(addButton('js-btn-copy', 'Copy memo', 'fa-regular fa-copy'));
1013
+ break;
1014
+ }
1015
+ case 'code': {
1016
+ const wrapBtn = addButton(`js-btn-wordwrap ${isWordWrap ? '' : 'active-nowrap'}`, isWordWrap ? 'Disable word wrap' : 'Enable word wrap', isWordWrap ? 'fa-solid fa-align-left' : 'fa-solid fa-arrows-left-right');
1017
+ container.appendChild(wrapBtn);
1018
+ container.appendChild(addButton('js-btn-edit', 'Edit content', 'fa-solid fa-pen-to-square'));
1019
+ container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1020
+ container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1021
+ container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1022
+ break;
1023
+ }
1024
+ case 'markdown': {
1025
+ container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1026
+ container.appendChild(addButton('js-btn-fit-text-height', 'Fit text height', 'fa-solid fa-arrows-up-down'));
1027
+ container.appendChild(addButton('disabled', 'Share (coming soon)', 'fa-solid fa-share-nodes', true));
1028
+ break;
1029
+ }
1030
1030
  case 'file': {
1031
1031
  container.appendChild(addButton('js-btn-copy', 'Copy content', 'fa-regular fa-copy'));
1032
1032
  break;
@@ -1034,20 +1034,20 @@
1034
1034
  case 'directory': {
1035
1035
  container.appendChild(addButton('js-btn-settings', 'Folder settings', 'fa-solid fa-gear'));
1036
1036
  break;
1037
- }
1038
- case 'image': {
1039
- container.appendChild(addButton('js-btn-copy-image', 'Copy image to clipboard', 'fa-regular fa-copy'));
1040
- break;
1041
- }
1042
- case 'video': {
1043
- container.appendChild(addButton('js-btn-video-play', 'Play video', 'fa-solid fa-play'));
1044
- container.appendChild(addButton('js-btn-video-pause', 'Pause video', 'fa-solid fa-pause'));
1045
- break;
1046
- }
1047
- }
1048
-
1049
- addSeparator();
1050
- container.appendChild(addButton('delete-btn js-btn-delete', 'Delete', 'fa-solid fa-trash'));
1037
+ }
1038
+ case 'image': {
1039
+ container.appendChild(addButton('js-btn-copy-image', 'Copy image to clipboard', 'fa-regular fa-copy'));
1040
+ break;
1041
+ }
1042
+ case 'video': {
1043
+ container.appendChild(addButton('js-btn-video-play', 'Play video', 'fa-solid fa-play'));
1044
+ container.appendChild(addButton('js-btn-video-pause', 'Pause video', 'fa-solid fa-pause'));
1045
+ break;
1046
+ }
1047
+ }
1048
+
1049
+ addSeparator();
1050
+ container.appendChild(addButton('delete-btn js-btn-delete', 'Delete', 'fa-solid fa-trash'));
1051
1051
 
1052
1052
  return container;
1053
1053
  }
@@ -1061,91 +1061,91 @@
1061
1061
  }
1062
1062
 
1063
1063
  // ▼▼▼ [New] Screen-space position using worldToScreen ▼▼▼
1064
- function worldToScreen(worldX, worldY, camera, container) {
1065
- const THREE = globalThis.THREE;
1066
- const vector = new THREE.Vector3(worldX, worldY, 0);
1067
- vector.project(camera);
1068
-
1069
- const viewportWidth = Number(_module?._lastViewportWidth || _module?._lastContainerWidth || container.clientWidth || 1);
1070
- const viewportHeight = Number(_module?._lastViewportHeight || _module?._lastContainerHeight || container.clientHeight || 1);
1071
- const viewportLeft = Number(_module?._lastViewportLeft || 0);
1072
- const viewportTop = Number(_module?._lastViewportTop || 0);
1073
- const halfWidth = viewportWidth / 2;
1074
- const halfHeight = viewportHeight / 2;
1075
-
1076
- return {
1077
- x: viewportLeft + (vector.x * halfWidth) + halfWidth,
1078
- y: viewportTop - (vector.y * halfHeight) + halfHeight
1079
- };
1080
- }
1081
-
1082
- function updateMenuScreenPosition(menuEl, nodeEntry, cameraMotionOnly = false) {
1083
- if (!menuEl || !nodeEntry || !_module || !_module.camera || !_module.container) return;
1084
-
1085
- const templateNode = document.getElementById(`node-${nodeEntry.model?.id || currentMenuNodeId}`);
1086
- let anchorX = null;
1087
- let anchorY = null;
1088
- const shouldPreferProjectedAnchor = cameraMotionOnly === true ||
1089
- _module.isZooming === true ||
1090
- _module.isPanning === true ||
1091
- _module.isDraggingNode === true ||
1092
- _module.isDraggingMultipleNodes === true ||
1093
- _module.isResizing === true;
1094
-
1095
- if (templateNode && !shouldPreferProjectedAnchor) {
1096
- const rect = templateNode.getBoundingClientRect();
1097
- if (Number.isFinite(rect.left) && Number.isFinite(rect.top) && rect.width > 0 && rect.height > 0) {
1098
- anchorX = rect.left + rect.width / 2;
1099
- anchorY = rect.top;
1100
- }
1101
- }
1102
-
1103
- if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
1104
- const model = nodeEntry.model || {};
1105
- const glObject = nodeEntry.glObject || null;
1106
-
1107
- // Use live world position during drag to keep menu attached to moving node.
1108
- const posX = glObject ? glObject.position.x : (model.positionX || 0);
1109
- const posY = glObject ? glObject.position.y : (model.positionY || 0);
1110
-
1111
- // Prefer runtime width when available (resize/drag in-progress), fallback to model width.
1112
- const liveWidth = glObject?.userData?.width;
1113
- const width = Number.isFinite(liveWidth) ? liveWidth : (model.width || 400);
1114
-
1115
- // Node top-center in world coordinates
1116
- const centerX = posX + width / 2;
1117
- const topY = posY;
1118
- const screenPos = worldToScreen(centerX, topY, _module.camera, _module.container);
1119
- anchorX = screenPos.x;
1120
- anchorY = screenPos.y;
1121
- }
1122
-
1123
- let halfMenuWidth = Number(menuEl._mindMapHalfWidth || 0);
1124
- let menuHeight = Number(menuEl._mindMapHeight || 0);
1125
- if (cameraMotionOnly !== true || halfMenuWidth <= 0 || menuHeight <= 0) {
1126
- const menuRect = menuEl.getBoundingClientRect();
1127
- halfMenuWidth = menuRect.width > 0 ? menuRect.width / 2 : 0;
1128
- menuHeight = menuRect.height || 0;
1129
- menuEl._mindMapHalfWidth = halfMenuWidth;
1130
- menuEl._mindMapHeight = menuHeight;
1131
- }
1132
- const viewportMargin = 10;
1133
- const verticalGap = 14;
1134
-
1135
- const clampedLeft = Math.min(
1136
- window.innerWidth - viewportMargin - halfMenuWidth,
1137
- Math.max(viewportMargin + halfMenuWidth, anchorX)
1138
- );
1139
- const anchoredTop = anchorY - verticalGap;
1140
- const minTop = viewportMargin + menuHeight;
1141
- const clampedTop = Math.max(minTop, anchoredTop);
1142
-
1143
- menuEl.style.left = `${clampedLeft}px`;
1144
- menuEl.style.top = `${clampedTop}px`;
1145
- }
1064
+ function worldToScreen(worldX, worldY, camera, container) {
1065
+ const THREE = globalThis.THREE;
1066
+ const vector = new THREE.Vector3(worldX, worldY, 0);
1067
+ vector.project(camera);
1068
+
1069
+ const viewportWidth = Number(_module?._lastViewportWidth || _module?._lastContainerWidth || container.clientWidth || 1);
1070
+ const viewportHeight = Number(_module?._lastViewportHeight || _module?._lastContainerHeight || container.clientHeight || 1);
1071
+ const viewportLeft = Number(_module?._lastViewportLeft || 0);
1072
+ const viewportTop = Number(_module?._lastViewportTop || 0);
1073
+ const halfWidth = viewportWidth / 2;
1074
+ const halfHeight = viewportHeight / 2;
1075
+
1076
+ return {
1077
+ x: viewportLeft + (vector.x * halfWidth) + halfWidth,
1078
+ y: viewportTop - (vector.y * halfHeight) + halfHeight
1079
+ };
1080
+ }
1081
+
1082
+ function updateMenuScreenPosition(menuEl, nodeEntry, cameraMotionOnly = false) {
1083
+ if (!menuEl || !nodeEntry || !_module || !_module.camera || !_module.container) return;
1084
+
1085
+ const templateNode = document.getElementById(`node-${nodeEntry.model?.id || currentMenuNodeId}`);
1086
+ let anchorX = null;
1087
+ let anchorY = null;
1088
+ const shouldPreferProjectedAnchor = cameraMotionOnly === true ||
1089
+ _module.isZooming === true ||
1090
+ _module.isPanning === true ||
1091
+ _module.isDraggingNode === true ||
1092
+ _module.isDraggingMultipleNodes === true ||
1093
+ _module.isResizing === true;
1094
+
1095
+ if (templateNode && !shouldPreferProjectedAnchor) {
1096
+ const rect = templateNode.getBoundingClientRect();
1097
+ if (Number.isFinite(rect.left) && Number.isFinite(rect.top) && rect.width > 0 && rect.height > 0) {
1098
+ anchorX = rect.left + rect.width / 2;
1099
+ anchorY = rect.top;
1100
+ }
1101
+ }
1102
+
1103
+ if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
1104
+ const model = nodeEntry.model || {};
1105
+ const glObject = nodeEntry.glObject || null;
1106
+
1107
+ // Use live world position during drag to keep menu attached to moving node.
1108
+ const posX = glObject ? glObject.position.x : (model.positionX || 0);
1109
+ const posY = glObject ? glObject.position.y : (model.positionY || 0);
1110
+
1111
+ // Prefer runtime width when available (resize/drag in-progress), fallback to model width.
1112
+ const liveWidth = glObject?.userData?.width;
1113
+ const width = Number.isFinite(liveWidth) ? liveWidth : (model.width || 400);
1114
+
1115
+ // Node top-center in world coordinates
1116
+ const centerX = posX + width / 2;
1117
+ const topY = posY;
1118
+ const screenPos = worldToScreen(centerX, topY, _module.camera, _module.container);
1119
+ anchorX = screenPos.x;
1120
+ anchorY = screenPos.y;
1121
+ }
1122
+
1123
+ let halfMenuWidth = Number(menuEl._mindMapHalfWidth || 0);
1124
+ let menuHeight = Number(menuEl._mindMapHeight || 0);
1125
+ if (cameraMotionOnly !== true || halfMenuWidth <= 0 || menuHeight <= 0) {
1126
+ const menuRect = menuEl.getBoundingClientRect();
1127
+ halfMenuWidth = menuRect.width > 0 ? menuRect.width / 2 : 0;
1128
+ menuHeight = menuRect.height || 0;
1129
+ menuEl._mindMapHalfWidth = halfMenuWidth;
1130
+ menuEl._mindMapHeight = menuHeight;
1131
+ }
1132
+ const viewportMargin = 10;
1133
+ const verticalGap = 14;
1134
+
1135
+ const clampedLeft = Math.min(
1136
+ window.innerWidth - viewportMargin - halfMenuWidth,
1137
+ Math.max(viewportMargin + halfMenuWidth, anchorX)
1138
+ );
1139
+ const anchoredTop = anchorY - verticalGap;
1140
+ const minTop = viewportMargin + menuHeight;
1141
+ const clampedTop = Math.max(minTop, anchoredTop);
1142
+
1143
+ menuEl.style.left = `${clampedLeft}px`;
1144
+ menuEl.style.top = `${clampedTop}px`;
1145
+ }
1146
1146
  // ▲▲▲ [New] ▲▲▲
1147
1147
 
1148
- function update() {
1148
+ function update() {
1149
1149
  if (currentMenuElement && currentMenuNodeId && _module) {
1150
1150
  const nodeEntry = _module.nodeObjectsById.get(currentMenuNodeId);
1151
1151
  if (nodeEntry) {
@@ -1156,37 +1156,37 @@
1156
1156
  }
1157
1157
  }
1158
1158
 
1159
- function bindMenuEvents(element, nodeId, nodeEntry) {
1160
- const invokeMenuAction = async (button, methodName, ...args) => {
1161
- if (!button || !_module?.dotNetHelper || button.disabled) {
1162
- return;
1163
- }
1164
-
1165
- button.disabled = true;
1166
- hideMenu();
1167
-
1168
- try {
1169
- await _module.dotNetHelper.invokeMethodAsync(methodName, ...args);
1170
- } catch (error) {
1171
- console.error(`[MindMapMenuManager] ${methodName} failed for node ${nodeId}`, error);
1172
- } finally {
1173
- button.disabled = false;
1174
- }
1175
- };
1176
-
1177
- element.addEventListener('mousedown', (e) => {
1178
- if (!e.target.closest('.js-btn-color')) {
1179
- element.querySelectorAll('.menu-color-popover.is-open').forEach(popover => {
1180
- popover.classList.remove('is-open');
1181
- });
1182
- }
1183
- });
1184
-
1185
- // AI Toggle
1186
- const aiBtn = element.querySelector('.js-btn-ai');
1187
- if (aiBtn) {
1188
- aiBtn.addEventListener('click', (e) => {
1189
- e.stopPropagation();
1159
+ function bindMenuEvents(element, nodeId, nodeEntry) {
1160
+ const invokeMenuAction = async (button, methodName, ...args) => {
1161
+ if (!button || !_module?.dotNetHelper || button.disabled) {
1162
+ return;
1163
+ }
1164
+
1165
+ button.disabled = true;
1166
+ hideMenu();
1167
+
1168
+ try {
1169
+ await _module.dotNetHelper.invokeMethodAsync(methodName, ...args);
1170
+ } catch (error) {
1171
+ console.error(`[MindMapMenuManager] ${methodName} failed for node ${nodeId}`, error);
1172
+ } finally {
1173
+ button.disabled = false;
1174
+ }
1175
+ };
1176
+
1177
+ element.addEventListener('mousedown', (e) => {
1178
+ if (!e.target.closest('.js-btn-color')) {
1179
+ element.querySelectorAll('.menu-color-popover.is-open').forEach(popover => {
1180
+ popover.classList.remove('is-open');
1181
+ });
1182
+ }
1183
+ });
1184
+
1185
+ // AI Toggle
1186
+ const aiBtn = element.querySelector('.js-btn-ai');
1187
+ if (aiBtn) {
1188
+ aiBtn.addEventListener('click', (e) => {
1189
+ e.stopPropagation();
1190
1190
  nodeEntry.model.isAiEnabled = !nodeEntry.model.isAiEnabled;
1191
1191
  if (_module.dotNetHelper) _module.dotNetHelper.invokeMethodAsync('ToggleNodeAi', nodeId);
1192
1192
 
@@ -1228,114 +1228,114 @@
1228
1228
  }
1229
1229
 
1230
1230
  // Copy (text)
1231
- const copyBtn = element.querySelector('.js-btn-copy');
1232
- if (copyBtn) {
1233
- copyBtn.addEventListener('click', (e) => {
1234
- e.stopPropagation();
1235
- const isMemo = nodeEntry.model.contentType === 'memo';
1236
- const metadataText = isMemo
1237
- ? [nodeEntry.model.prompt || '', nodeEntry.model.response || ''].filter(Boolean).join('\n\n')
1238
- : (nodeEntry.model.response || '');
1239
- const text = metadataText;
1240
- navigator.clipboard.writeText(text).then(() => {
1241
- showActionSuccessIcon(copyBtn);
1242
- });
1243
- });
1244
- }
1245
-
1246
- const fitTextHeightBtn = element.querySelector('.js-btn-fit-text-height');
1247
- if (fitTextHeightBtn && isTextFitNode(nodeEntry.model)) {
1248
- fitTextHeightBtn.addEventListener('click', async (e) => {
1249
- e.stopPropagation();
1250
- fitTextHeightBtn.disabled = true;
1251
- try {
1252
- await window.MindMapMultiSelect?.fitTextNodesToContentAndReflowY?.([nodeId]);
1253
- const icon = fitTextHeightBtn.querySelector('i');
1254
- if (icon) {
1255
- const oldClass = icon.className;
1256
- icon.className = 'fa-solid fa-check text-green-500';
1257
- setTimeout(() => icon.className = oldClass, 1000);
1258
- }
1259
- requestAnimationFrame(() => update());
1260
- } finally {
1261
- fitTextHeightBtn.disabled = false;
1262
- }
1263
- });
1264
- }
1265
-
1266
- const colorBtn = element.querySelector('.js-btn-color');
1267
- const colorPopover = colorBtn?.querySelector('.menu-color-popover');
1268
- if (colorBtn && colorPopover) {
1269
- syncMemoColorPopover(colorPopover, getMemoColorKey(nodeEntry.model));
1270
-
1271
- colorBtn.addEventListener('click', (e) => {
1272
- e.stopPropagation();
1273
- colorPopover.classList.toggle('is-open');
1274
- });
1275
-
1276
- colorPopover.querySelectorAll('.menu-color-swatch').forEach(button => {
1277
- button.addEventListener('click', (e) => {
1278
- e.stopPropagation();
1279
- const colorKey = button.dataset.colorKey || 'coral';
1280
- const metadata = ensureMetadata(nodeEntry.model);
1281
- metadata.memoColor = colorKey;
1282
- metadata.memoColorIsCustom = 'true';
1283
- nodeEntry.model.metadata = metadata;
1284
- nodeEntry.model.Metadata = metadata;
1285
- syncMemoColorPopover(colorPopover, colorKey);
1286
- colorPopover.classList.remove('is-open');
1287
- refreshMemoNode(nodeEntry);
1288
- if (_module.dotNetHelper) {
1289
- _module.dotNetHelper.invokeMethodAsync('UpdateNodeMetadata', nodeId, 'memoColor', colorKey);
1290
- }
1291
- });
1292
- });
1293
- }
1231
+ const copyBtn = element.querySelector('.js-btn-copy');
1232
+ if (copyBtn) {
1233
+ copyBtn.addEventListener('click', (e) => {
1234
+ e.stopPropagation();
1235
+ const isMemo = nodeEntry.model.contentType === 'memo';
1236
+ const metadataText = isMemo
1237
+ ? [nodeEntry.model.prompt || '', nodeEntry.model.response || ''].filter(Boolean).join('\n\n')
1238
+ : (nodeEntry.model.response || '');
1239
+ const text = metadataText;
1240
+ navigator.clipboard.writeText(text).then(() => {
1241
+ showActionSuccessIcon(copyBtn);
1242
+ });
1243
+ });
1244
+ }
1245
+
1246
+ const fitTextHeightBtn = element.querySelector('.js-btn-fit-text-height');
1247
+ if (fitTextHeightBtn && isTextFitNode(nodeEntry.model)) {
1248
+ fitTextHeightBtn.addEventListener('click', async (e) => {
1249
+ e.stopPropagation();
1250
+ fitTextHeightBtn.disabled = true;
1251
+ try {
1252
+ await window.MindMapMultiSelect?.fitTextNodesToContentAndReflowY?.([nodeId]);
1253
+ const icon = fitTextHeightBtn.querySelector('i');
1254
+ if (icon) {
1255
+ const oldClass = icon.className;
1256
+ icon.className = 'fa-solid fa-check text-green-500';
1257
+ setTimeout(() => icon.className = oldClass, 1000);
1258
+ }
1259
+ requestAnimationFrame(() => update());
1260
+ } finally {
1261
+ fitTextHeightBtn.disabled = false;
1262
+ }
1263
+ });
1264
+ }
1265
+
1266
+ const colorBtn = element.querySelector('.js-btn-color');
1267
+ const colorPopover = colorBtn?.querySelector('.menu-color-popover');
1268
+ if (colorBtn && colorPopover) {
1269
+ syncMemoColorPopover(colorPopover, getMemoColorKey(nodeEntry.model));
1270
+
1271
+ colorBtn.addEventListener('click', (e) => {
1272
+ e.stopPropagation();
1273
+ colorPopover.classList.toggle('is-open');
1274
+ });
1275
+
1276
+ colorPopover.querySelectorAll('.menu-color-swatch').forEach(button => {
1277
+ button.addEventListener('click', (e) => {
1278
+ e.stopPropagation();
1279
+ const colorKey = button.dataset.colorKey || 'coral';
1280
+ const metadata = ensureMetadata(nodeEntry.model);
1281
+ metadata.memoColor = colorKey;
1282
+ metadata.memoColorIsCustom = 'true';
1283
+ nodeEntry.model.metadata = metadata;
1284
+ nodeEntry.model.Metadata = metadata;
1285
+ syncMemoColorPopover(colorPopover, colorKey);
1286
+ colorPopover.classList.remove('is-open');
1287
+ refreshMemoNode(nodeEntry);
1288
+ if (_module.dotNetHelper) {
1289
+ _module.dotNetHelper.invokeMethodAsync('UpdateNodeMetadata', nodeId, 'memoColor', colorKey);
1290
+ }
1291
+ });
1292
+ });
1293
+ }
1294
1294
 
1295
1295
  // Copy image
1296
- const copyImgBtn = element.querySelector('.js-btn-copy-image');
1297
- if (copyImgBtn) {
1298
- copyImgBtn.addEventListener('click', async (e) => {
1299
- e.stopPropagation();
1300
- const candidateSources = getImageClipboardSourceCandidates(nodeEntry.model);
1301
- if (candidateSources.length === 0) {
1302
- showToast('No image to copy.', { kind: 'error' });
1303
- return;
1304
- }
1305
-
1306
- try {
1307
- const { blob, src } = await fetchClipboardImageBlob(nodeEntry.model);
1308
- const pngBlob = await convertBlobToPngForClipboard(blob);
1309
- const copiedKind = await writeImageBlobToClipboard(pngBlob, src);
1310
-
1311
- if (copiedKind !== 'image') {
1312
- showToast('Image clipboard is unavailable in this browser. Copied the image URL instead.');
1313
- return;
1314
- }
1315
-
1316
- console.log(`[MindMapMenuManager] Image copied to clipboard as PNG (${pngBlob.size} bytes) from ${src}`);
1317
- showActionSuccessIcon(copyImgBtn);
1318
- } catch (err) {
1319
- console.error('Image copy failed:', err);
1320
- showToast('Failed to copy image to the clipboard.', { kind: 'error' });
1321
- }
1322
- });
1323
- }
1296
+ const copyImgBtn = element.querySelector('.js-btn-copy-image');
1297
+ if (copyImgBtn) {
1298
+ copyImgBtn.addEventListener('click', async (e) => {
1299
+ e.stopPropagation();
1300
+ const candidateSources = getImageClipboardSourceCandidates(nodeEntry.model);
1301
+ if (candidateSources.length === 0) {
1302
+ showToast('No image to copy.', { kind: 'error' });
1303
+ return;
1304
+ }
1305
+
1306
+ try {
1307
+ const { blob, src } = await fetchClipboardImageBlob(nodeEntry.model);
1308
+ const pngBlob = await convertBlobToPngForClipboard(blob);
1309
+ const copiedKind = await writeImageBlobToClipboard(pngBlob, src);
1310
+
1311
+ if (copiedKind !== 'image') {
1312
+ showToast('Image clipboard is unavailable in this browser. Copied the image URL instead.');
1313
+ return;
1314
+ }
1315
+
1316
+ console.log(`[MindMapMenuManager] Image copied to clipboard as PNG (${pngBlob.size} bytes) from ${src}`);
1317
+ showActionSuccessIcon(copyImgBtn);
1318
+ } catch (err) {
1319
+ console.error('Image copy failed:', err);
1320
+ showToast('Failed to copy image to the clipboard.', { kind: 'error' });
1321
+ }
1322
+ });
1323
+ }
1324
1324
 
1325
1325
  // Delete
1326
1326
  const delBtn = element.querySelector('.js-btn-delete');
1327
- if (delBtn) {
1328
- delBtn.addEventListener('click', (e) => {
1329
- e.stopPropagation();
1330
- const snapshot =
1331
- window.MindMapNodes?.captureDeleteSnapshot?.(_module, nodeId) ||
1332
- { nodeId: String(nodeId), currentContent: nodeEntry.model.response || '' };
1333
- hideMenu();
1334
- if (_module.dotNetHelper) {
1335
- _module.dotNetHelper.invokeMethodAsync('DeleteSingleNodeSnapshotFromJs', snapshot);
1336
- }
1337
- });
1338
- }
1327
+ if (delBtn) {
1328
+ delBtn.addEventListener('click', (e) => {
1329
+ e.stopPropagation();
1330
+ const snapshot =
1331
+ window.MindMapNodes?.captureDeleteSnapshot?.(_module, nodeId) ||
1332
+ { nodeId: String(nodeId), currentContent: nodeEntry.model.response || '' };
1333
+ hideMenu();
1334
+ if (_module.dotNetHelper) {
1335
+ _module.dotNetHelper.invokeMethodAsync('DeleteSingleNodeSnapshotFromJs', snapshot);
1336
+ }
1337
+ });
1338
+ }
1339
1339
 
1340
1340
  // Settings
1341
1341
  const settingBtn = element.querySelector('.js-btn-settings');
@@ -1347,10 +1347,10 @@
1347
1347
  }
1348
1348
 
1349
1349
  // Duplicate button (for single image)
1350
- const duplicateBtn = element.querySelector('.js-btn-duplicate');
1351
- if (duplicateBtn) {
1352
- duplicateBtn.addEventListener('click', (e) => {
1353
- e.stopPropagation();
1350
+ const duplicateBtn = element.querySelector('.js-btn-duplicate');
1351
+ if (duplicateBtn) {
1352
+ duplicateBtn.addEventListener('click', (e) => {
1353
+ e.stopPropagation();
1354
1354
  if (_module.dotNetHelper) {
1355
1355
  _module.dotNetHelper.invokeMethodAsync('DuplicateNodeFromJs', nodeId);
1356
1356
  }
@@ -1360,30 +1360,30 @@
1360
1360
  i.className = 'fa-solid fa-check text-green-500';
1361
1361
  setTimeout(() => i.className = oldClass, 1000);
1362
1362
  }
1363
- });
1364
- }
1365
-
1366
- const videoPlayBtn = element.querySelector('.js-btn-video-play');
1367
- if (videoPlayBtn) {
1368
- videoPlayBtn.addEventListener('click', (e) => {
1369
- e.stopPropagation();
1370
- window.MindMapCss3DManager?.setVideoNodePlayback?.(nodeId, true);
1371
- });
1372
- }
1373
-
1374
- const videoPauseBtn = element.querySelector('.js-btn-video-pause');
1375
- if (videoPauseBtn) {
1376
- videoPauseBtn.addEventListener('click', (e) => {
1377
- e.stopPropagation();
1378
- window.MindMapCss3DManager?.setVideoNodePlayback?.(nodeId, false);
1379
- });
1380
- }
1381
-
1382
- // ▼▼▼ [NEW] Word wrap toggle for code nodes ▼▼▼
1383
- const wrapBtn = element.querySelector('.js-btn-wordwrap');
1384
- if (wrapBtn) {
1385
- wrapBtn.addEventListener('click', (e) => {
1386
- e.stopPropagation();
1363
+ });
1364
+ }
1365
+
1366
+ const videoPlayBtn = element.querySelector('.js-btn-video-play');
1367
+ if (videoPlayBtn) {
1368
+ videoPlayBtn.addEventListener('click', (e) => {
1369
+ e.stopPropagation();
1370
+ window.MindMapCss3DManager?.setVideoNodePlayback?.(nodeId, true);
1371
+ });
1372
+ }
1373
+
1374
+ const videoPauseBtn = element.querySelector('.js-btn-video-pause');
1375
+ if (videoPauseBtn) {
1376
+ videoPauseBtn.addEventListener('click', (e) => {
1377
+ e.stopPropagation();
1378
+ window.MindMapCss3DManager?.setVideoNodePlayback?.(nodeId, false);
1379
+ });
1380
+ }
1381
+
1382
+ // ▼▼▼ [NEW] Word wrap toggle for code nodes ▼▼▼
1383
+ const wrapBtn = element.querySelector('.js-btn-wordwrap');
1384
+ if (wrapBtn) {
1385
+ wrapBtn.addEventListener('click', (e) => {
1386
+ e.stopPropagation();
1387
1387
  const newWrapState = !nodeEntry.model.isWordWrap;
1388
1388
  nodeEntry.model.isWordWrap = newWrapState;
1389
1389
  console.log(`[MindMapMenuManager] Word wrap toggled for ${nodeId}: ${newWrapState}`);
@@ -1404,169 +1404,169 @@
1404
1404
  if (window.MindMapNodes && window.MindMapNodes.updateNodeWordWrap) {
1405
1405
  window.MindMapNodes.updateNodeWordWrap(_module, nodeId, newWrapState);
1406
1406
  }
1407
- });
1408
- }
1409
- // ▲▲▲ [NEW] ▲▲▲
1410
-
1411
- const nodeRunBtn = element.querySelector('.js-btn-node-run');
1412
- if (nodeRunBtn) {
1413
- nodeRunBtn.addEventListener('click', async (e) => {
1414
- e.stopPropagation();
1415
- if (!_module?.dotNetHelper || nodeRunBtn.disabled) {
1416
- return;
1417
- }
1418
-
1419
- nodeRunBtn.disabled = true;
1420
- nodeRunBtn.classList.add('disabled');
1421
- setExecutableNodeRunVisualState(nodeId, 'running', 'Running');
1422
- hideMenu();
1423
-
1424
- try {
1425
- const result = await _module.dotNetHelper.invokeMethodAsync('RunExecutableNodeFromJs', nodeId);
1426
- await syncNodeRunResultStates(result);
1427
- if (result?.success === false && !Array.isArray(result?.nodeStates)) {
1428
- setExecutableNodeRunVisualState(nodeId, 'idle', 'Failed');
1429
- }
1430
- if (result?.success === false) {
1431
- window.MindMapFeedback?.toast?.(result.message || 'Node run failed.', { kind: 'error' });
1432
- }
1433
- } catch (error) {
1434
- console.error(`[MindMapMenuManager] RunExecutableNodeFromJs failed for node ${nodeId}`, error);
1435
- setExecutableNodeRunVisualState(nodeId, 'idle', 'Failed');
1436
- window.MindMapFeedback?.toast?.('Node run failed.', { kind: 'error' });
1437
- } finally {
1438
- nodeRunBtn.disabled = false;
1439
- nodeRunBtn.classList.remove('disabled');
1440
- }
1441
- });
1442
- }
1443
-
1444
- const companySyncGoalBtn = element.querySelector('.js-btn-company-sync-goal');
1445
- if (companySyncGoalBtn) {
1446
- companySyncGoalBtn.addEventListener('click', async (e) => {
1447
- e.stopPropagation();
1448
- if (!_module?.dotNetHelper || companySyncGoalBtn.disabled) {
1449
- return;
1450
- }
1451
-
1452
- companySyncGoalBtn.disabled = true;
1453
- applyCompanyBindingValues(nodeEntry.model, {
1454
- 'CompanyCore.LastMessage': 'Updating the existing company goal from this note...',
1455
- 'CompanyCore.Status': getCompanyStatus(nodeEntry.model) || 'Running'
1456
- });
1457
- syncCompanyNodeVisual(nodeEntry);
1458
- showMenu(nodeId);
1459
-
1460
- try {
1461
- const mode = getCompanyBindingValue(nodeEntry.model, 'CompanyCore.ExecutionMode') || 'Auto';
1462
- const subAgentsEnabled = getCompanyBindingValue(nodeEntry.model, 'CompanyCore.SubAgentsEnabled') !== 'false';
1463
- const result = await _module.dotNetHelper.invokeMethodAsync('SyncCompanyGoalFromJs', nodeId, mode, subAgentsEnabled);
1464
- applyCompanyRunResult(nodeEntry, result, mode, subAgentsEnabled);
1465
- } catch (error) {
1466
- console.error(`[MindMapMenuManager] SyncCompanyGoalFromJs failed for node ${nodeId}`, error);
1467
- applyCompanyBindingValues(nodeEntry.model, {
1468
- 'CompanyCore.LastMessage': 'Updating the company goal failed.',
1469
- 'CompanyCore.Status': 'Failed'
1470
- });
1471
- } finally {
1472
- syncCompanyNodeVisual(nodeEntry);
1473
- showMenu(nodeId);
1474
- }
1475
- });
1476
- }
1477
-
1478
- const companyGraphBtn = element.querySelector('.js-btn-company-graph');
1479
- if (companyGraphBtn) {
1480
- companyGraphBtn.addEventListener('click', async (e) => {
1481
- e.stopPropagation();
1482
- await invokeMenuAction(companyGraphBtn, 'OpenCompanyGraphFromJs', nodeId);
1483
- });
1484
- }
1485
-
1486
- const companyRefreshBtn = element.querySelector('.js-btn-company-refresh');
1487
- if (companyRefreshBtn) {
1488
- companyRefreshBtn.addEventListener('click', async (e) => {
1489
- e.stopPropagation();
1490
- await invokeMenuAction(companyRefreshBtn, 'RefreshCompanyBindingFromJs', nodeId);
1491
- });
1492
- }
1493
-
1494
- const companyPauseBtn = element.querySelector('.js-btn-company-pause');
1495
- if (companyPauseBtn) {
1496
- companyPauseBtn.addEventListener('click', async (e) => {
1497
- e.stopPropagation();
1498
- await invokeMenuAction(companyPauseBtn, 'PauseCompanyFromJs', nodeId);
1499
- });
1500
- }
1501
-
1502
- const companyResumeBtn = element.querySelector('.js-btn-company-resume');
1503
- if (companyResumeBtn) {
1504
- companyResumeBtn.addEventListener('click', async (e) => {
1505
- e.stopPropagation();
1506
- await invokeMenuAction(companyResumeBtn, 'ResumeCompanyFromJs', nodeId);
1507
- });
1508
- }
1509
-
1510
- const companyArchiveBtn = element.querySelector('.js-btn-company-archive');
1511
- if (companyArchiveBtn) {
1512
- companyArchiveBtn.addEventListener('click', async (e) => {
1513
- e.stopPropagation();
1514
- await invokeMenuAction(companyArchiveBtn, 'ArchiveCompanyFromJs', nodeId);
1515
- });
1516
- }
1517
-
1518
- const companyDetachBtn = element.querySelector('.js-btn-company-detach');
1519
- if (companyDetachBtn) {
1520
- companyDetachBtn.addEventListener('click', async (e) => {
1521
- e.stopPropagation();
1522
- await invokeMenuAction(companyDetachBtn, 'DetachCompanyFromJs', nodeId);
1523
- });
1524
- }
1525
-
1526
- const companyRemoveBtn = element.querySelector('.js-btn-company-remove');
1527
- if (companyRemoveBtn) {
1528
- companyRemoveBtn.addEventListener('click', async (e) => {
1529
- e.stopPropagation();
1530
- if (!_module?.dotNetHelper || companyRemoveBtn.disabled) {
1531
- return;
1532
- }
1533
-
1534
- const shouldRemove = await showConfirmDialog({
1535
- title: 'Remove company?',
1536
- message: 'The company will be archived and detached from this node.',
1537
- confirmText: 'Remove',
1538
- cancelText: 'Cancel'
1539
- });
1540
- if (!shouldRemove) {
1541
- return;
1542
- }
1543
-
1544
- companyRemoveBtn.disabled = true;
1545
- try {
1546
- await _module.dotNetHelper.invokeMethodAsync('RemoveCompanyFromJs', nodeId);
1547
- clearCompanyBindingValues(nodeEntry);
1548
- } catch (error) {
1549
- console.error(`[MindMapMenuManager] RemoveCompanyFromJs failed for node ${nodeId}`, error);
1550
- } finally {
1551
- syncCompanyNodeVisual(nodeEntry);
1552
- showMenu(nodeId);
1553
- }
1554
- });
1555
- }
1556
- }
1557
-
1558
- function updateForCameraMotion() {
1559
- if (!currentMenuElement || !currentMenuNodeId || !_module) return false;
1560
- const nodeEntry = _module.nodeObjectsById.get(currentMenuNodeId);
1561
- if (!nodeEntry) {
1562
- hideMenu();
1563
- return false;
1564
- }
1565
- updateMenuScreenPosition(currentMenuElement, nodeEntry, true);
1566
- return true;
1567
- }
1568
-
1569
- window.MindMapMenuManager = { init, showMenu, hideMenu, update, updateForCameraMotion };
1407
+ });
1408
+ }
1409
+ // ▲▲▲ [NEW] ▲▲▲
1410
+
1411
+ const nodeRunBtn = element.querySelector('.js-btn-node-run');
1412
+ if (nodeRunBtn) {
1413
+ nodeRunBtn.addEventListener('click', async (e) => {
1414
+ e.stopPropagation();
1415
+ if (!_module?.dotNetHelper || nodeRunBtn.disabled) {
1416
+ return;
1417
+ }
1418
+
1419
+ nodeRunBtn.disabled = true;
1420
+ nodeRunBtn.classList.add('disabled');
1421
+ setExecutableNodeRunVisualState(nodeId, 'running', 'Running');
1422
+ hideMenu();
1423
+
1424
+ try {
1425
+ const result = await _module.dotNetHelper.invokeMethodAsync('RunExecutableNodeFromJs', nodeId);
1426
+ await syncNodeRunResultStates(result);
1427
+ if (result?.success === false && !Array.isArray(result?.nodeStates)) {
1428
+ setExecutableNodeRunVisualState(nodeId, 'idle', 'Failed');
1429
+ }
1430
+ if (result?.success === false) {
1431
+ window.MindMapFeedback?.toast?.(result.message || 'Node run failed.', { kind: 'error' });
1432
+ }
1433
+ } catch (error) {
1434
+ console.error(`[MindMapMenuManager] RunExecutableNodeFromJs failed for node ${nodeId}`, error);
1435
+ setExecutableNodeRunVisualState(nodeId, 'idle', 'Failed');
1436
+ window.MindMapFeedback?.toast?.('Node run failed.', { kind: 'error' });
1437
+ } finally {
1438
+ nodeRunBtn.disabled = false;
1439
+ nodeRunBtn.classList.remove('disabled');
1440
+ }
1441
+ });
1442
+ }
1443
+
1444
+ const companySyncGoalBtn = element.querySelector('.js-btn-company-sync-goal');
1445
+ if (companySyncGoalBtn) {
1446
+ companySyncGoalBtn.addEventListener('click', async (e) => {
1447
+ e.stopPropagation();
1448
+ if (!_module?.dotNetHelper || companySyncGoalBtn.disabled) {
1449
+ return;
1450
+ }
1451
+
1452
+ companySyncGoalBtn.disabled = true;
1453
+ applyCompanyBindingValues(nodeEntry.model, {
1454
+ 'CompanyCore.LastMessage': 'Updating the existing company goal from this note...',
1455
+ 'CompanyCore.Status': getCompanyStatus(nodeEntry.model) || 'Running'
1456
+ });
1457
+ syncCompanyNodeVisual(nodeEntry);
1458
+ showMenu(nodeId);
1459
+
1460
+ try {
1461
+ const mode = getCompanyBindingValue(nodeEntry.model, 'CompanyCore.ExecutionMode') || 'Auto';
1462
+ const subAgentsEnabled = getCompanyBindingValue(nodeEntry.model, 'CompanyCore.SubAgentsEnabled') !== 'false';
1463
+ const result = await _module.dotNetHelper.invokeMethodAsync('SyncCompanyGoalFromJs', nodeId, mode, subAgentsEnabled);
1464
+ applyCompanyRunResult(nodeEntry, result, mode, subAgentsEnabled);
1465
+ } catch (error) {
1466
+ console.error(`[MindMapMenuManager] SyncCompanyGoalFromJs failed for node ${nodeId}`, error);
1467
+ applyCompanyBindingValues(nodeEntry.model, {
1468
+ 'CompanyCore.LastMessage': 'Updating the company goal failed.',
1469
+ 'CompanyCore.Status': 'Failed'
1470
+ });
1471
+ } finally {
1472
+ syncCompanyNodeVisual(nodeEntry);
1473
+ showMenu(nodeId);
1474
+ }
1475
+ });
1476
+ }
1477
+
1478
+ const companyGraphBtn = element.querySelector('.js-btn-company-graph');
1479
+ if (companyGraphBtn) {
1480
+ companyGraphBtn.addEventListener('click', async (e) => {
1481
+ e.stopPropagation();
1482
+ await invokeMenuAction(companyGraphBtn, 'OpenCompanyGraphFromJs', nodeId);
1483
+ });
1484
+ }
1485
+
1486
+ const companyRefreshBtn = element.querySelector('.js-btn-company-refresh');
1487
+ if (companyRefreshBtn) {
1488
+ companyRefreshBtn.addEventListener('click', async (e) => {
1489
+ e.stopPropagation();
1490
+ await invokeMenuAction(companyRefreshBtn, 'RefreshCompanyBindingFromJs', nodeId);
1491
+ });
1492
+ }
1493
+
1494
+ const companyPauseBtn = element.querySelector('.js-btn-company-pause');
1495
+ if (companyPauseBtn) {
1496
+ companyPauseBtn.addEventListener('click', async (e) => {
1497
+ e.stopPropagation();
1498
+ await invokeMenuAction(companyPauseBtn, 'PauseCompanyFromJs', nodeId);
1499
+ });
1500
+ }
1501
+
1502
+ const companyResumeBtn = element.querySelector('.js-btn-company-resume');
1503
+ if (companyResumeBtn) {
1504
+ companyResumeBtn.addEventListener('click', async (e) => {
1505
+ e.stopPropagation();
1506
+ await invokeMenuAction(companyResumeBtn, 'ResumeCompanyFromJs', nodeId);
1507
+ });
1508
+ }
1509
+
1510
+ const companyArchiveBtn = element.querySelector('.js-btn-company-archive');
1511
+ if (companyArchiveBtn) {
1512
+ companyArchiveBtn.addEventListener('click', async (e) => {
1513
+ e.stopPropagation();
1514
+ await invokeMenuAction(companyArchiveBtn, 'ArchiveCompanyFromJs', nodeId);
1515
+ });
1516
+ }
1517
+
1518
+ const companyDetachBtn = element.querySelector('.js-btn-company-detach');
1519
+ if (companyDetachBtn) {
1520
+ companyDetachBtn.addEventListener('click', async (e) => {
1521
+ e.stopPropagation();
1522
+ await invokeMenuAction(companyDetachBtn, 'DetachCompanyFromJs', nodeId);
1523
+ });
1524
+ }
1525
+
1526
+ const companyRemoveBtn = element.querySelector('.js-btn-company-remove');
1527
+ if (companyRemoveBtn) {
1528
+ companyRemoveBtn.addEventListener('click', async (e) => {
1529
+ e.stopPropagation();
1530
+ if (!_module?.dotNetHelper || companyRemoveBtn.disabled) {
1531
+ return;
1532
+ }
1533
+
1534
+ const shouldRemove = await showConfirmDialog({
1535
+ title: 'Remove company?',
1536
+ message: 'The company will be archived and detached from this node.',
1537
+ confirmText: 'Remove',
1538
+ cancelText: 'Cancel'
1539
+ });
1540
+ if (!shouldRemove) {
1541
+ return;
1542
+ }
1543
+
1544
+ companyRemoveBtn.disabled = true;
1545
+ try {
1546
+ await _module.dotNetHelper.invokeMethodAsync('RemoveCompanyFromJs', nodeId);
1547
+ clearCompanyBindingValues(nodeEntry);
1548
+ } catch (error) {
1549
+ console.error(`[MindMapMenuManager] RemoveCompanyFromJs failed for node ${nodeId}`, error);
1550
+ } finally {
1551
+ syncCompanyNodeVisual(nodeEntry);
1552
+ showMenu(nodeId);
1553
+ }
1554
+ });
1555
+ }
1556
+ }
1557
+
1558
+ function updateForCameraMotion() {
1559
+ if (!currentMenuElement || !currentMenuNodeId || !_module) return false;
1560
+ const nodeEntry = _module.nodeObjectsById.get(currentMenuNodeId);
1561
+ if (!nodeEntry) {
1562
+ hideMenu();
1563
+ return false;
1564
+ }
1565
+ updateMenuScreenPosition(currentMenuElement, nodeEntry, true);
1566
+ return true;
1567
+ }
1568
+
1569
+ window.MindMapMenuManager = { init, showMenu, hideMenu, update, updateForCameraMotion };
1570
1570
 
1571
1571
  console.log('✅ mind-map-menu-manager.js loaded');
1572
1572
  })();