@mindexec/cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,14 @@ By default the CLI opens the MindCanvas app in your default browser after the
18
18
  bridge is ready. Use `mindexec --no-open` or `MINDEXEC_NO_OPEN=1` when you only
19
19
  want to start the listener.
20
20
 
21
+ For npx and global installs, the default workspace is the directory where you
22
+ run the command. Run it from the folder that contains your `.mindexec` data, or
23
+ pass the workspace explicitly:
24
+
25
+ ```bash
26
+ npx @mindexec/cli --workspace /path/to/workspace
27
+ ```
28
+
21
29
  For local frontend development, keep LocalBridge running and refresh the
22
30
  packaged app bundle after code changes:
23
31
 
@@ -47,6 +55,7 @@ mindexec
47
55
  湲곕낯 ?ㅽ뻾 二쇱냼??`http://127.0.0.1:5147`?낅땲?? `WORKSPACE_PATH`媛€ 鍮꾩뼱 ?덉쑝硫??ㅼ튂???⑦궎吏€??遺€紐??대뜑瑜?湲곕낯 ?묒뾽 怨듦컙?쇰줈 ?ъ슜?⑸땲??
48
56
 
49
57
  ```bash
58
+ mindexec --workspace /custom/path
50
59
  BRIDGE_PORT=8080 WORKSPACE_PATH=/custom/path mindexec
51
60
  ```
52
61
 
package/launch-bridge.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const path = require('path');
4
+ const fs = require('fs');
4
5
  const { spawn } = require('child_process');
5
6
  const packageInfo = require('./package.json');
6
7
  const { normalizePort, releaseBridgePort } = require('./port-guard.cjs');
@@ -8,23 +9,94 @@ const { normalizePort, releaseBridgePort } = require('./port-guard.cjs');
8
9
  const bridgeRoot = __dirname;
9
10
  const serverPath = path.resolve(bridgeRoot, 'server.js');
10
11
  const inheritedWorkspacePath = String(process.env.WORKSPACE_PATH || '').trim();
11
- const workspacePath = inheritedWorkspacePath || path.resolve(bridgeRoot, '..');
12
12
  const rawArgs = process.argv.slice(2);
13
+ const parsedArgs = extractWorkspaceArg(rawArgs);
14
+ const cliArgs = parsedArgs.args;
15
+ const workspacePath = path.resolve(
16
+ parsedArgs.workspacePath || inheritedWorkspacePath || getDefaultWorkspacePath()
17
+ );
13
18
  const appUrl = `http://localhost:${normalizePort(process.env.BRIDGE_PORT)}/mindcanvas`;
14
19
  const statusUrl = `http://127.0.0.1:${normalizePort(process.env.BRIDGE_PORT)}/api/status`;
15
20
 
21
+ function isSamePath(left, right) {
22
+ return path.resolve(left).toLowerCase() === path.resolve(right).toLowerCase();
23
+ }
24
+
25
+ function pathExists(...segments) {
26
+ return fs.existsSync(path.join(...segments));
27
+ }
28
+
29
+ function looksLikeWorkspaceRoot(candidate) {
30
+ return pathExists(candidate, '.mindexec')
31
+ || pathExists(candidate, 'MindExecution.Web')
32
+ || pathExists(candidate, 'MindExecution.Shared')
33
+ || pathExists(candidate, 'LocalBridge');
34
+ }
35
+
36
+ function getDefaultWorkspacePath() {
37
+ const cwd = process.cwd();
38
+ const bridgeParent = path.resolve(bridgeRoot, '..');
39
+
40
+ if (isSamePath(cwd, bridgeRoot) && looksLikeWorkspaceRoot(bridgeParent)) {
41
+ return bridgeParent;
42
+ }
43
+
44
+ return cwd;
45
+ }
46
+
47
+ function extractWorkspaceArg(args) {
48
+ const filteredArgs = [];
49
+ let workspacePath = '';
50
+
51
+ for (let index = 0; index < args.length; index += 1) {
52
+ const arg = args[index];
53
+ if (arg === '--workspace' || arg === '--workspace-path') {
54
+ workspacePath = String(args[index + 1] || '').trim();
55
+ if (!workspacePath) {
56
+ console.error(`[MindExec CLI] ${arg} requires a path.`);
57
+ process.exit(1);
58
+ }
59
+
60
+ index += 1;
61
+ continue;
62
+ }
63
+
64
+ if (arg.startsWith('--workspace=')) {
65
+ workspacePath = arg.slice('--workspace='.length).trim();
66
+ continue;
67
+ }
68
+
69
+ if (arg.startsWith('--workspace-path=')) {
70
+ workspacePath = arg.slice('--workspace-path='.length).trim();
71
+ continue;
72
+ }
73
+
74
+ filteredArgs.push(arg);
75
+ }
76
+
77
+ return {
78
+ args: filteredArgs,
79
+ workspacePath
80
+ };
81
+ }
82
+
16
83
  function printHelp() {
17
84
  console.log(`MindExec CLI ${packageInfo.version}
18
85
 
19
86
  Usage:
20
- mindexec [start] [node-options]
21
- npx @mindexec/cli
87
+ mindexec [start] [options] [node-options]
88
+ npx @mindexec/cli [options]
22
89
 
23
90
  Runs the MindExec local app and bridge on http://localhost:5147/mindcanvas by default.
24
91
 
92
+ Options:
93
+ --workspace <path>
94
+ Workspace root. Defaults to the current directory for npx/global runs
95
+ --no-open Do not open the local app in the default browser
96
+
25
97
  Environment:
26
98
  BRIDGE_PORT Local bridge port. Default: 5147
27
- WORKSPACE_PATH Workspace root. Default: parent of the installed package
99
+ WORKSPACE_PATH Workspace root. Overridden by --workspace when both are set
28
100
  BRIDGE_TOKEN Fixed token for protected REST APIs
29
101
  MINDEXEC_WEB_ROOT Published MindCanvas wwwroot override. Default: package wwwroot
30
102
  MINDEXEC_NO_OPEN=1
@@ -37,25 +109,27 @@ Environment:
37
109
  Examples:
38
110
  mindexec
39
111
  mindexec --no-open
112
+ mindexec --workspace /path/to/work
113
+ npx @mindexec/cli --workspace /path/to/work
40
114
  BRIDGE_PORT=8080 WORKSPACE_PATH=/path/to/work mindexec
41
115
  mindexec --watch
42
116
  `);
43
117
  }
44
118
 
45
- if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
119
+ if (cliArgs.includes('--help') || cliArgs.includes('-h')) {
46
120
  printHelp();
47
121
  process.exit(0);
48
122
  }
49
123
 
50
- if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
124
+ if (cliArgs.includes('--version') || cliArgs.includes('-v')) {
51
125
  console.log(packageInfo.version);
52
126
  process.exit(0);
53
127
  }
54
128
 
55
- const command = rawArgs[0];
129
+ const command = cliArgs[0];
56
130
  const commandArgs = command === 'start' || command === 'bridge'
57
- ? rawArgs.slice(1)
58
- : rawArgs;
131
+ ? cliArgs.slice(1)
132
+ : cliArgs;
59
133
  const shouldOpenApp = !commandArgs.some(arg => arg === '--no-open' || arg === '--no-browser')
60
134
  && !/^(1|true|yes|on)$/i.test(String(process.env.MINDEXEC_NO_OPEN || '').trim())
61
135
  && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_OPEN_APP || '').trim());
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
- "files": [
8
- "start-bridge.bat",
9
- "start-bridge.sh",
10
- "launch-bridge.cjs",
11
- "server.js",
12
- "codex-runtime.js",
13
- "port-guard.cjs",
14
- "wwwroot/",
15
- "scripts/",
7
+ "files": [
8
+ "start-bridge.bat",
9
+ "start-bridge.sh",
10
+ "launch-bridge.cjs",
11
+ "server.js",
12
+ "codex-runtime.js",
13
+ "port-guard.cjs",
14
+ "wwwroot/",
15
+ "scripts/",
16
16
  "tree-sitter-grammars/",
17
17
  "README.md"
18
18
  ],
19
19
  "scripts": {
20
20
  "start": "node launch-bridge.cjs",
21
21
  "dev": "node launch-bridge.cjs --watch",
22
- "test:syntax": "node --check server.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs",
22
+ "test:syntax": "node --check server.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs",
23
23
  "pack:dry": "npm pack --dry-run",
24
24
  "setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
25
25
  "postinstall": "npm run setup:grammars"
@@ -36,6 +36,17 @@
36
36
  contain-intrinsic-size: auto !important;
37
37
  }
38
38
 
39
+ .css3d-resolution-wrapper.node-type-templatelauncher,
40
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node,
41
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node-template-card {
42
+ contain: layout style !important;
43
+ content-visibility: visible !important;
44
+ contain-intrinsic-size: auto !important;
45
+ isolation: isolate !important;
46
+ -webkit-backface-visibility: visible !important;
47
+ backface-visibility: visible !important;
48
+ }
49
+
39
50
  .css3d-resolution-wrapper.node-type-image > .map-node,
40
51
  .css3d-resolution-wrapper.node-type-video > .map-node,
41
52
  .css3d-resolution-wrapper.node-type-embed > .map-node {
@@ -5,7 +5,7 @@
5
5
  const DEBUG = false;
6
6
  const FPS_DEBUG = false;
7
7
  const FRAME_PERF_DEBUG = false;
8
- const MINDMAP_CORE_BUILD_ID = '20260609-midfar-idle-resident-wake-v204';
8
+ const MINDMAP_CORE_BUILD_ID = '20260610-template-board-ready-v205';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -170,6 +170,52 @@
170
170
  let moduleInstance = null;
171
171
  let pendingAuthToken = null;
172
172
  let pendingSettings = null;
173
+ let _lastCompletedBoardLoadId = null;
174
+ let _lastCompletedBoardLoadAt = 0;
175
+
176
+ function normalizeBoardIdValue(boardId) {
177
+ return String(boardId ?? '').trim().toLowerCase();
178
+ }
179
+
180
+ function invalidateCurrentBoardLoadCompletion(reason = 'unknown') {
181
+ _lastCompletedBoardLoadId = null;
182
+ _lastCompletedBoardLoadAt = 0;
183
+ emitMindCanvasTrace('board.load.pending', {
184
+ boardId: _currentBoardId,
185
+ reason
186
+ });
187
+ }
188
+
189
+ function isCurrentBoardLoadComplete() {
190
+ const activeBoardId = normalizeBoardIdValue(_currentBoardId);
191
+ return !!activeBoardId && activeBoardId === _lastCompletedBoardLoadId;
192
+ }
193
+
194
+ function markBoardLoadComplete(boardId, reason = 'unknown') {
195
+ const expectedBoardId = normalizeBoardIdValue(boardId);
196
+ const activeBoardId = normalizeBoardIdValue(_currentBoardId);
197
+ if (!expectedBoardId || expectedBoardId !== activeBoardId) {
198
+ emitMindCanvasTrace('board.staleMutationBlocked', {
199
+ mutation: 'markBoardLoadComplete',
200
+ expectedBoardId,
201
+ activeBoardId,
202
+ reason
203
+ });
204
+ return false;
205
+ }
206
+
207
+ _lastCompletedBoardLoadId = expectedBoardId;
208
+ _lastCompletedBoardLoadAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
209
+ emitMindCanvasTrace('board.load.complete', {
210
+ boardId: _currentBoardId,
211
+ reason,
212
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
213
+ isLoading: moduleInstance?.isLoading === true,
214
+ phase: moduleInstance?.canvasPhase || null
215
+ });
216
+ return true;
217
+ }
218
+
173
219
  function isDocumentHidden() {
174
220
  return typeof document !== 'undefined' && document.visibilityState === 'hidden';
175
221
  }
@@ -1997,7 +2043,11 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
1997
2043
  let _currentBoardId = null;
1998
2044
 
1999
2045
  function setActiveBoardIdRuntime(boardId, reason = 'unknown') {
2046
+ const previousBoardId = _currentBoardId;
2000
2047
  _currentBoardId = boardId || null;
2048
+ if (normalizeBoardIdValue(previousBoardId) !== normalizeBoardIdValue(_currentBoardId)) {
2049
+ invalidateCurrentBoardLoadCompletion(reason);
2050
+ }
2001
2051
  window.MindCanvasBuildInfo?.setRuntime?.('activeBoardId', _currentBoardId);
2002
2052
  emitMindCanvasTrace('board.active', {
2003
2053
  boardId: _currentBoardId,
@@ -7258,6 +7308,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
7258
7308
  if (boardId) {
7259
7309
  setActiveBoardIdRuntime(boardId, 'resetBoard');
7260
7310
  }
7311
+ invalidateCurrentBoardLoadCompletion('resetBoard');
7261
7312
  // ▲▲▲ [Fix] ▲▲▲
7262
7313
  const resetPhase = moduleInstance.canvasPhase === CanvasPhase.Booting
7263
7314
  ? CanvasPhase.BoardFileLoading
@@ -7369,9 +7420,22 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
7369
7420
  // ▼▼▼ [Fix] Expose activeBoardId for async guard in addNode ▼▼▼
7370
7421
  get activeBoardId() { return _currentBoardId; },
7371
7422
  setActiveBoardId: (boardId) => { setActiveBoardIdRuntime(boardId, 'external-setActiveBoardId'); },
7423
+ markBoardLoadCompleteForBoard: (boardId) => markBoardLoadComplete(boardId, 'csharp-board-load-complete'),
7424
+ getActiveBoardRuntimeState: () => ({
7425
+ activeBoardId: _currentBoardId,
7426
+ activeBoardIdNormalized: normalizeBoardIdValue(_currentBoardId),
7427
+ isLoading: moduleInstance?.isLoading === true,
7428
+ canvasPhase: moduleInstance?.canvasPhase || null,
7429
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
7430
+ boardLoadComplete: isCurrentBoardLoadComplete(),
7431
+ boardLoadCompletedAt: _lastCompletedBoardLoadAt || 0
7432
+ }),
7372
7433
  getRuntimeDiagnostics: () => ({
7373
7434
  activeBoardId: _currentBoardId,
7374
7435
  canvasPhase: moduleInstance?.canvasPhase || null,
7436
+ isLoading: moduleInstance?.isLoading === true,
7437
+ boardLoadComplete: isCurrentBoardLoadComplete(),
7438
+ nodeCount: moduleInstance?.nodeObjectsById?.size || 0,
7375
7439
  buildInfo: window.MindCanvasBuildInfo?.dump?.() || null,
7376
7440
  guardCounts: window.MindCanvasDevGuards?.getCounts?.() || {},
7377
7441
  trace: {
@@ -297,6 +297,17 @@
297
297
  overflow: hidden !important;
298
298
  }
299
299
 
300
+ .css3d-resolution-wrapper.node-type-templatelauncher,
301
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node,
302
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node-template-card {
303
+ contain: layout style !important;
304
+ content-visibility: visible !important;
305
+ contain-intrinsic-size: auto !important;
306
+ isolation: isolate !important;
307
+ backface-visibility: visible !important;
308
+ -webkit-backface-visibility: visible !important;
309
+ }
310
+
300
311
  .css3d-resolution-wrapper.node-type-image .node-content-wrapper,
301
312
  .css3d-resolution-wrapper.node-type-video .node-content-wrapper,
302
313
  .css3d-resolution-wrapper.node-type-embed .node-content-wrapper,
@@ -3196,6 +3207,8 @@
3196
3207
 
3197
3208
  nodeModel.response = normalized;
3198
3209
  nodeModel.Response = normalized;
3210
+ mediaEl.dataset.mediaSourceUrl = normalized;
3211
+ mediaEl.dataset.resolvedMediaSourceUrl = normalized;
3199
3212
  mediaEl.setAttribute('src', normalized);
3200
3213
 
3201
3214
  if (contentTypeLower === 'video' && typeof mediaEl.load === 'function') {
@@ -3266,11 +3279,25 @@
3266
3279
  return;
3267
3280
  }
3268
3281
 
3282
+ const clearVideoPlaybackError = () => {
3283
+ if (contentTypeLower !== 'video') {
3284
+ return;
3285
+ }
3286
+
3287
+ const metadata = ensureCssMediaMetadata(nodeModel);
3288
+ delete metadata.VideoPlaybackError;
3289
+ delete metadata.videoPlaybackError;
3290
+ delete metadata.VideoPlaybackErrorCode;
3291
+ delete metadata.videoPlaybackErrorCode;
3292
+ mediaEl.closest?.('.video-content')?.classList?.remove?.('mind-map-video-error');
3293
+ };
3294
+
3269
3295
  const handleReady = () => {
3270
3296
  if (contentTypeLower === 'image') {
3271
3297
  mediaEl.dataset.mediaReady = '1';
3272
3298
  mediaEl.style.opacity = '1';
3273
3299
  }
3300
+ clearVideoPlaybackError();
3274
3301
  setAssetRefreshState(nodeModel, '', '');
3275
3302
  syncMediaLoadingRowForElement(mediaEl, nodeModel, contentTypeLower);
3276
3303
  mediaEl.dataset.assetRefreshAttempted = '';
@@ -3282,12 +3309,61 @@
3282
3309
  }
3283
3310
  };
3284
3311
 
3285
- mediaEl.onerror = () => {
3312
+ const describeVideoElementError = () => {
3313
+ const code = Number(mediaEl?.error?.code || 0);
3314
+ const reason = {
3315
+ 1: 'loading was aborted',
3316
+ 2: 'a network error stopped loading',
3317
+ 3: 'the browser could not decode the media',
3318
+ 4: 'the browser does not support this codec or container'
3319
+ }[code] || 'the browser could not load the media';
3320
+ const source = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3321
+ let hint = 'Try H.264/AAC MP4 or WebM.';
3322
+ try {
3323
+ const parsed = new URL(source, window.location?.href || undefined);
3324
+ const extension = parsed.pathname.split('.').pop()?.toLowerCase() || '';
3325
+ if (extension === 'mov' || extension === 'm4v') {
3326
+ hint = 'MOV/M4V often fails in browsers unless encoded as H.264/AAC.';
3327
+ }
3328
+ } catch { }
3329
+
3330
+ return `Video playback failed: ${reason}. ${hint}`;
3331
+ };
3332
+
3333
+ const markVideoElementError = () => {
3334
+ if (contentTypeLower !== 'video') {
3335
+ return;
3336
+ }
3337
+
3338
+ const metadata = ensureCssMediaMetadata(nodeModel);
3339
+ const message = describeVideoElementError();
3340
+ metadata.VideoPlaybackError = message;
3341
+ metadata.videoPlaybackError = message;
3342
+ metadata.VideoPlaybackErrorCode = String(Number(mediaEl?.error?.code || 0));
3343
+ metadata.videoPlaybackErrorCode = metadata.VideoPlaybackErrorCode;
3344
+ mediaEl.closest?.('.video-content')?.classList?.add?.('mind-map-video-error');
3345
+ setAssetRefreshState(nodeModel, 'failed', message);
3346
+ syncMediaLoadingRowForElement(mediaEl, nodeModel, contentTypeLower);
3347
+ };
3348
+
3349
+ const handleMediaError = async () => {
3286
3350
  if (contentTypeLower === 'image') {
3287
3351
  mediaEl.dataset.mediaReady = '0';
3288
3352
  mediaEl.style.opacity = '0';
3289
3353
  }
3290
- void tryRefreshCssMediaSource(mediaEl, nodeModel, contentTypeLower);
3354
+ const beforeSource = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3355
+ await tryRefreshCssMediaSource(mediaEl, nodeModel, contentTypeLower);
3356
+ if (contentTypeLower === 'video') {
3357
+ const afterSource = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3358
+ const refreshState = getAssetRefreshState(nodeModel);
3359
+ if (afterSource === beforeSource && refreshState !== 'refreshing') {
3360
+ markVideoElementError();
3361
+ }
3362
+ }
3363
+ };
3364
+
3365
+ mediaEl.onerror = () => {
3366
+ void handleMediaError();
3291
3367
  };
3292
3368
 
3293
3369
  if (contentTypeLower === 'video') {
@@ -5740,7 +5816,7 @@
5740
5816
  tone = 'refresh';
5741
5817
  } else if ((contentTypeLower === 'image' || contentTypeLower === 'video') && assetRefreshState === 'failed') {
5742
5818
  showRow = true;
5743
- label = 'Asset refresh failed';
5819
+ label = contentTypeLower === 'video' ? 'Video playback failed' : 'Asset refresh failed';
5744
5820
  message = assetRefreshMessage || 'Reload the board or reopen this file.';
5745
5821
  tone = 'failed';
5746
5822
  } else if (contentTypeLower === 'video') {
@@ -367,6 +367,10 @@
367
367
  isAgentLikeLodNode(model);
368
368
  }
369
369
 
370
+ function isTemplateLauncherModel(model) {
371
+ return getNodeContentType(model) === 'templatelauncher';
372
+ }
373
+
370
374
  function isCssMediaFallbackModel(model) {
371
375
  const contentType = getNodeContentType(model);
372
376
  return contentType === 'image' ||
@@ -710,6 +714,7 @@
710
714
  function supportsCss3dNodeModel(model) {
711
715
  const contentType = getNodeContentType(model);
712
716
  return isTextLikeModel(model) ||
717
+ isTemplateLauncherModel(model) ||
713
718
  contentType === 'image' ||
714
719
  contentType === 'video' ||
715
720
  contentType === 'embed';
@@ -4379,6 +4384,15 @@
4379
4384
  this._warmCss3dNodeIds.has(nodeId);
4380
4385
  }
4381
4386
 
4387
+ _syncAlwaysCss3dNodes(nodeObjectsById, module = this._module) {
4388
+ if (!nodeObjectsById || !isCss3dRendererEnabled(module)) return;
4389
+
4390
+ for (const entry of nodeObjectsById.values()) {
4391
+ if (!entry || !isTemplateLauncherModel(entry.model)) continue;
4392
+ this._showNodeCss3d(entry, module);
4393
+ }
4394
+ }
4395
+
4382
4396
  _showNodeCss3d(entry, module) {
4383
4397
  if (!entry || !window.MindMapCss3DManager) return;
4384
4398
  const model = entry.model;
@@ -5517,6 +5531,8 @@
5517
5531
  // ▲▲▲ [FIX] ▲▲▲
5518
5532
 
5519
5533
 
5534
+ this._syncAlwaysCss3dNodes(nodeObjectsById, module);
5535
+
5520
5536
  } else {
5521
5537
  perfTime('fullResSetup', () => {
5522
5538
  // Full-res: NEAR(DOM overlay + CSS3D) / NORMAL(CSS3D only)
@@ -38,6 +38,14 @@
38
38
  const NODE_INTERACTIVE_CONTENT_SELECTORS = `${NODE_TEXT_SELECTION_SELECTORS}, .embed-content, .embed-card, .embed-card *, .embed-action, .embed-play-button, iframe, .map-node-memo__icon-button, .map-node-memo__icon-option, .map-node-memo__icon-popover, .map-node-memo__agent-action, .map-node-memo__agent-result-link, .map-node-memo__agent-plan-panel, .map-node-memo__agent-plan-body, .map-node-memo__agent-plan-body *, .map-node-memo__agent-console-panel, .map-node-memo__agent-console-body, .map-node-memo__agent-console-body *, .map-node-memo__agent-console-resize, .map-node-memo__agent-console-resize *`;
39
39
  const MEMO_EDIT_ONLY_SELECTORS = '.map-node-memo__title, .map-node-memo__body, .map-node-memo__icon-button, .map-node-memo__icon-option, .map-node-memo__icon-popover';
40
40
 
41
+ function getNodeContentTypeLower(nodeModel) {
42
+ return String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').trim().toLowerCase();
43
+ }
44
+
45
+ function isTemplateLauncherNodeModel(nodeModel) {
46
+ return getNodeContentTypeLower(nodeModel) === 'templatelauncher';
47
+ }
48
+
41
49
  function getNodeMetadata(nodeModel) {
42
50
  return nodeModel?.metadata || nodeModel?.Metadata || null;
43
51
  }
@@ -2106,7 +2114,8 @@
2106
2114
  // ★ [Add] Safeguard: abort if factory is not loaded yet (avoid crash)
2107
2115
  // ★ [Add] Safeguard: abort if factory is not loaded yet (avoid crash)
2108
2116
  // ★ [수정] 텍스처가 필요 없는 CSS3D 타입(text, note, code, markdown)은 팩토리가 없어도 통과시킵니다.
2109
- const isCss3dType = nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'code' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'embed';
2117
+ const isTemplateLauncherNode = isTemplateLauncherNodeModel(nodeModel);
2118
+ const isCss3dType = nodeModel.contentType === 'text' || nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'code' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'embed' || isTemplateLauncherNode;
2110
2119
 
2111
2120
  if (!isCss3dType && typeof window.MindMapTextureFactory === 'undefined') {
2112
2121
  console.error(`[MindMapNodes] Critical: MindMapTextureFactory is missing! Cannot add node ${nodeModel.id}. Retrying in 100ms...`);
@@ -2170,7 +2179,7 @@
2170
2179
  let autoPositioned = false;
2171
2180
  const isNewNode = nodeModel.metadata && nodeModel.metadata['IsNew'] === 'true';
2172
2181
 
2173
- if (isNewNode && (nodeModel.contentType === 'note' || nodeModel.contentType === 'text' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed') && nodeModel.positionX === 0 && nodeModel.positionY === 0) {
2182
+ if (isNewNode && (nodeModel.contentType === 'note' || nodeModel.contentType === 'text' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed' || isTemplateLauncherNode) && nodeModel.positionX === 0 && nodeModel.positionY === 0) {
2174
2183
  if (module.cursorPosition) {
2175
2184
  nodeModel.positionX = module.cursorPosition.x;
2176
2185
  nodeModel.positionY = module.cursorPosition.y;
@@ -2685,7 +2694,7 @@
2685
2694
  const isNearMode = module.lodRenderer ? !module.lodRenderer.isInLODMode : true;
2686
2695
 
2687
2696
  // Rich cards use CSS3D in NEAR/NORMAL so wrapper glow/selection stays consistent.
2688
- if (nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'text' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'code' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed') {
2697
+ if (nodeModel.contentType === 'note' || nodeModel.contentType === 'memo' || nodeModel.contentType === 'text' || nodeModel.contentType === 'markdown' || nodeModel.contentType === 'code' || nodeModel.contentType === 'image' || nodeModel.contentType === 'video' || nodeModel.contentType === 'embed' || isTemplateLauncherNode) {
2689
2698
  log(`[MindMapNodes] Creating cssObject for ${nodeModel.contentType} node ${nodeModel.id}`);
2690
2699
  const shouldDeferCssObject = module.isLoading && (
2691
2700
  nodeModel.contentType === 'text' ||
@@ -2704,7 +2713,7 @@
2704
2713
  const glPos = glObject.position;
2705
2714
  // [FIX] Parallax issue fix: Set Z to match glObject (was + 0.2)
2706
2715
  nodeEntry.cssObject.position.set(glPos.x, glPos.y, glPos.z);
2707
- const shouldAttachCss = isNearMode;
2716
+ const shouldAttachCss = isNearMode || isTemplateLauncherNode;
2708
2717
  if (shouldAttachCss) {
2709
2718
  (module.cssScene || module.scene).add(nodeEntry.cssObject);
2710
2719
  window.MindMapCss3DManager?.markCssParentRepairNeeded?.(module, nodeModel.id);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-j9lSCQnAHXGmdzDIi3d5whli8CN6n66zmJphjRM2/ZI=",
4
+ "hash": "sha256-bpjahUAizPTcZ5/jYUUN2m7UstGT3kbK9FOnfweXUO8=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.gwwc40sc45.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.0jgrn1sckv.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.13mme2qcag.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.dfp2mdt45q.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.9al2g3v3f9.dll": "MindExecution.Plugins.Concept.dll",
131
131
  "MindExecution.Plugins.Directory.3w4t6n3se0.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.s0qpntz420.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.iu11fq8d16.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.7j27dcqnrc.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.pq1ty8ov2v.dll": "MindExecution.Web.dll",
132
+ "MindExecution.Plugins.PlanMaster.vfmfbygv5y.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.32jyiqs383.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.7ttmykvopx.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.ozzcqp30uy.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.xsn1d6x2kd.js": "dotnet.native.js",
138
138
  "dotnet.native.vz0adxojrz.wasm": "dotnet.native.wasm",
@@ -280,16 +280,16 @@
280
280
  "netstandard.0xet7jg7ky.dll": "sha256-xENDv620uJ8fHwLJ2bdhrTHz4QPjvqXOztnk2a4wr0c=",
281
281
  "MindExecution.Core.1q1trifbuu.dll": "sha256-Bx+DnvKBnQ6pU+WkVnBtHJWmbT8+AfwdrzIsZOYRuwk=",
282
282
  "MindExecution.Kernel.gwwc40sc45.dll": "sha256-kHUYRsoWMKBagWUNqjc2tZDNhinix4z4JbCHhmQqr+w=",
283
- "MindExecution.Plugins.Concept.dfp2mdt45q.dll": "sha256-OecaUsIn1VlvDL5wBdzpYm2qwLSvYFEubsTNZD5cqWc=",
284
- "MindExecution.Plugins.PlanMaster.s0qpntz420.dll": "sha256-/IqukDrp/IN9xLgL2Tk49cFCKvhUil8tz9EED052KwM=",
285
- "MindExecution.Shared.7j27dcqnrc.dll": "sha256-k0X8TvkgyDcpJk03YwVNKPYEN/oQ1BhAXH22CtP7lmM=",
286
- "MindExecution.Web.pq1ty8ov2v.dll": "sha256-74+tLbyshIXRRBzB0l2KvysS0+DKpaZ4/JQ9KYf3lMo="
283
+ "MindExecution.Plugins.Concept.9al2g3v3f9.dll": "sha256-QYQcZjx/1n94cDfUijDLZP+Mb0V8qN/BXwg+Yabys7U=",
284
+ "MindExecution.Plugins.PlanMaster.vfmfbygv5y.dll": "sha256-W5LWlsDtp+V4lm7BLdhefVcNkfRJ8b67uF7CWh5vtng=",
285
+ "MindExecution.Shared.7ttmykvopx.dll": "sha256-a2JiyNIN4cEhbaPuCSHbToBV2fLKbDK0/aEOrEB0vHs=",
286
+ "MindExecution.Web.ozzcqp30uy.dll": "sha256-NM/QHswprBwzOuOVX0bH1Q9dOfu0eLz784S5dFaMLT0="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.0jgrn1sckv.dll": "sha256-bGesH24e01NzlKAZSfHwmyCRSCqXh7Dh8IzpYUD/+vQ=",
290
290
  "MindExecution.Plugins.Business.13mme2qcag.dll": "sha256-Tc/4bEirMCQn6JXVksPprJln7V0VOUCHXOqdMYYoHOc=",
291
291
  "MindExecution.Plugins.Directory.3w4t6n3se0.dll": "sha256-A3Cd8T7bI7w2uz0HT8L6inpQp40EVGFAks1Hl7bPxo0=",
292
- "MindExecution.Plugins.YouTube.iu11fq8d16.dll": "sha256-Zmlfr3wCyo/K6+6+wKziZnEvv9qaffdxaegJmSDs2vM="
292
+ "MindExecution.Plugins.YouTube.32jyiqs383.dll": "sha256-BfHgivxHkj3qFVB0s5RJXV7nvvbJjpSue9Cnbcbx8co="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260610-video-dnd-v454" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260610-video-dnd-v454" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260610-template-card-v460" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260610-template-card-v460" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -558,7 +558,7 @@
558
558
  }
559
559
 
560
560
  const base = '_content/MindExecution.Shared/js/';
561
- const scriptVersion = '20260610-video-dnd-v454';
561
+ const scriptVersion = '20260610-template-card-v460';
562
562
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
563
563
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
564
564
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "DG6TAbxo",
2
+ "version": "IFXfuOYd",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -42,7 +42,7 @@
42
42
  "url": "_content/MindExecution.Shared/css/app.css"
43
43
  },
44
44
  {
45
- "hash": "sha256-aFRuE02nuyx5pmI+f1OphRTDxfeIfFOF6ZSpgUnjOzU=",
45
+ "hash": "sha256-8c4EBD7ciaWPLPydbFB44+kg/t5LYC0wVUO7GM0eHHs=",
46
46
  "url": "_content/MindExecution.Shared/css/mind-map-overrides.css"
47
47
  },
48
48
  {
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-1bN3G+Kvz//ajslfZkDbiB0JlOEqYE9zDfzUyMUuHM4=",
81
+ "hash": "sha256-Njstb0CPStc2mmPhfiAWmqPZEHooyzZ5DfcPfAXF21A=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-Gb2UEcBSTZegpBX8BUGB/2oW5sSu3I0zILX/kC74sU4=",
89
+ "hash": "sha256-tbkbEIXDKELigOD/5f5XEfRmaXg8Q7NJ/OpLyKC2gg0=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -114,7 +114,7 @@
114
114
  "url": "_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js"
115
115
  },
116
116
  {
117
- "hash": "sha256-ZK4kJVsno8A3B4Sy+deHjTqFtstV2eSJDt+jPKgcfuM=",
117
+ "hash": "sha256-eCnM98QtSxKZXrNczEE7YjT3uHmIVz9tee8R+33mMnc=",
118
118
  "url": "_content/MindExecution.Shared/js/mind-map-lod-renderer.js"
119
119
  },
120
120
  {
@@ -134,7 +134,7 @@
134
134
  "url": "_content/MindExecution.Shared/js/mind-map-node-search-worker.js"
135
135
  },
136
136
  {
137
- "hash": "sha256-yYoxPLbG2g8rBHwNyJBWZF/wi2hG0tQDXx3crliTTAA=",
137
+ "hash": "sha256-2qBxbVqSdyOUf9ZphBekCGWunuKiFOPRlK/vB2hiCBw=",
138
138
  "url": "_content/MindExecution.Shared/js/mind-map-nodes.js"
139
139
  },
140
140
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.13mme2qcag.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-OecaUsIn1VlvDL5wBdzpYm2qwLSvYFEubsTNZD5cqWc=",
430
- "url": "_framework/MindExecution.Plugins.Concept.dfp2mdt45q.dll"
429
+ "hash": "sha256-QYQcZjx/1n94cDfUijDLZP+Mb0V8qN/BXwg+Yabys7U=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.9al2g3v3f9.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-A3Cd8T7bI7w2uz0HT8L6inpQp40EVGFAks1Hl7bPxo0=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.3w4t6n3se0.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-/IqukDrp/IN9xLgL2Tk49cFCKvhUil8tz9EED052KwM=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.s0qpntz420.dll"
437
+ "hash": "sha256-W5LWlsDtp+V4lm7BLdhefVcNkfRJ8b67uF7CWh5vtng=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.vfmfbygv5y.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-Zmlfr3wCyo/K6+6+wKziZnEvv9qaffdxaegJmSDs2vM=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.iu11fq8d16.dll"
441
+ "hash": "sha256-BfHgivxHkj3qFVB0s5RJXV7nvvbJjpSue9Cnbcbx8co=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.32jyiqs383.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-k0X8TvkgyDcpJk03YwVNKPYEN/oQ1BhAXH22CtP7lmM=",
446
- "url": "_framework/MindExecution.Shared.7j27dcqnrc.dll"
445
+ "hash": "sha256-a2JiyNIN4cEhbaPuCSHbToBV2fLKbDK0/aEOrEB0vHs=",
446
+ "url": "_framework/MindExecution.Shared.7ttmykvopx.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-74+tLbyshIXRRBzB0l2KvysS0+DKpaZ4/JQ9KYf3lMo=",
450
- "url": "_framework/MindExecution.Web.pq1ty8ov2v.dll"
449
+ "hash": "sha256-NM/QHswprBwzOuOVX0bH1Q9dOfu0eLz784S5dFaMLT0=",
450
+ "url": "_framework/MindExecution.Web.ozzcqp30uy.dll"
451
451
  },
452
452
  {
453
453
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-GH60B592wM29SlKKi+ms1pJ+uIqnA43EzAvXuxBROk4=",
773
+ "hash": "sha256-1V8kJI8cQQC5wH7NDXOkR1Bl3ZCqG0lKzlAZShrzyJA=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-i5ricR/8J4W5bSLBVwDNqBQtUAoohO+DuD/UaR0agk4=",
837
+ "hash": "sha256-78bACARkTS2HDx0RnUgBr8qzgYhIv/9sm+az5Gi7nkk=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: DG6TAbxo */
1
+ /* Manifest version: IFXfuOYd */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4