@mindexec/cli 0.2.308 → 0.2.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.308",
3
+ "version": "0.2.310",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/server.js CHANGED
@@ -1871,6 +1871,135 @@ async function getRegisteredLocalFile(id) {
1871
1871
  return normalizedId ? localFileRegistry.get(normalizedId) || null : null;
1872
1872
  }
1873
1873
 
1874
+ function decodeAssetRequestPath(rawPath) {
1875
+ let value = String(rawPath || '').trim();
1876
+ if (!value) {
1877
+ return { requestPath: '', isThumbs: false };
1878
+ }
1879
+
1880
+ try {
1881
+ if (/^https?:\/\//i.test(value)) {
1882
+ value = new URL(value).pathname;
1883
+ }
1884
+ } catch {
1885
+ // Keep the original value and let the path safety checks reject it if needed.
1886
+ }
1887
+
1888
+ value = value.split(/[?#]/)[0].replaceAll('\\', '/');
1889
+ const lower = value.toLowerCase();
1890
+ const thumbsMarker = '/assets/thumbs/';
1891
+ const assetsMarker = '/assets/';
1892
+ let isThumbs = false;
1893
+ const thumbsIndex = lower.indexOf(thumbsMarker);
1894
+ const assetsIndex = lower.indexOf(assetsMarker);
1895
+
1896
+ if (thumbsIndex >= 0) {
1897
+ value = value.slice(thumbsIndex + thumbsMarker.length);
1898
+ isThumbs = true;
1899
+ } else if (assetsIndex >= 0) {
1900
+ value = value.slice(assetsIndex + assetsMarker.length);
1901
+ }
1902
+
1903
+ try {
1904
+ value = decodeURIComponent(value);
1905
+ } catch {
1906
+ // A malformed escape sequence should not crash the bridge.
1907
+ }
1908
+
1909
+ return {
1910
+ requestPath: value.replace(/^\/+/, ''),
1911
+ isThumbs
1912
+ };
1913
+ }
1914
+
1915
+ function encodeAssetUrlPath(relativePath) {
1916
+ return normalizePathForClient(relativePath)
1917
+ .replace(/^\/+/, '')
1918
+ .split('/')
1919
+ .filter(Boolean)
1920
+ .map(segment => encodeURIComponent(segment))
1921
+ .join('/');
1922
+ }
1923
+
1924
+ async function resolveAssetPathForRequest(normalizedRequestPath, options = {}) {
1925
+ const requestPath = String(normalizedRequestPath || '').replace(/^\/+/, '');
1926
+ if (!requestPath) {
1927
+ return { status: 'missing', resolvedPath: null };
1928
+ }
1929
+
1930
+ const isThumbs = options.thumbs === true;
1931
+ const primaryAssetsDir = getAssetsPath();
1932
+ const legacyAssetsDir = path.join(workspacePath, 'assets');
1933
+ const primaryDir = isThumbs ? path.join(primaryAssetsDir, 'thumbs') : primaryAssetsDir;
1934
+ const legacyDir = isThumbs ? path.join(legacyAssetsDir, 'thumbs') : legacyAssetsDir;
1935
+ const primaryPath = path.resolve(primaryDir, requestPath);
1936
+ const legacyPath = path.resolve(legacyDir, requestPath);
1937
+
1938
+ const candidates = [];
1939
+ if (isPathWithin(primaryDir, primaryPath)) {
1940
+ candidates.push(primaryPath);
1941
+ }
1942
+ if (isPathWithin(legacyDir, legacyPath)) {
1943
+ candidates.push(legacyPath);
1944
+ }
1945
+
1946
+ if (candidates.length === 0) {
1947
+ return { status: 'forbidden', resolvedPath: null };
1948
+ }
1949
+
1950
+ let resolvedPath = await findFirstAccessiblePath(candidates);
1951
+ if (!resolvedPath) {
1952
+ resolvedPath = await findFileByStem(
1953
+ [primaryDir, legacyDir],
1954
+ requestPath,
1955
+ isThumbs
1956
+ ? ['.jpg', '.jpeg', '.webp', '.png']
1957
+ : ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg', '.mp4', '.webm', '.mov']
1958
+ );
1959
+ }
1960
+
1961
+ if (!resolvedPath && isThumbs) {
1962
+ resolvedPath = await findFileByStem(
1963
+ [primaryAssetsDir, legacyAssetsDir],
1964
+ requestPath,
1965
+ ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
1966
+ );
1967
+ }
1968
+
1969
+ return {
1970
+ status: resolvedPath ? 'found' : 'missing',
1971
+ resolvedPath
1972
+ };
1973
+ }
1974
+
1975
+ app.get('/api/assets/resolve', async (req, res) => {
1976
+ const decoded = decodeAssetRequestPath(req.query?.path || req.query?.url || '');
1977
+ if (!decoded.requestPath) {
1978
+ return res.json({
1979
+ exists: false,
1980
+ reason: 'empty-path'
1981
+ });
1982
+ }
1983
+
1984
+ const result = await resolveAssetPathForRequest(decoded.requestPath, {
1985
+ thumbs: decoded.isThumbs
1986
+ });
1987
+
1988
+ if (result.status === 'forbidden') {
1989
+ return res.status(403).json({
1990
+ exists: false,
1991
+ reason: 'forbidden'
1992
+ });
1993
+ }
1994
+
1995
+ const urlPath = encodeAssetUrlPath(decoded.requestPath);
1996
+ return res.json({
1997
+ exists: result.status === 'found',
1998
+ url: `/${decoded.isThumbs ? 'assets/thumbs' : 'assets'}/${urlPath}`,
1999
+ path: decoded.requestPath
2000
+ });
2001
+ });
2002
+
1874
2003
  // Static file serving for assets (direct image loading - bypasses base64 encoding)
1875
2004
  // This allows browsers to directly fetch images via http://127.0.0.1:5147/assets/filename.png
1876
2005
  app.use('/assets', async (req, res, next) => {
@@ -1484,17 +1484,36 @@ html.mindcanvas-platform-apple .css3d-resolution-wrapper.is-automation-relation-
1484
1484
 
1485
1485
  .mind-map-text-overlay-v2-card.is-passive {
1486
1486
  pointer-events: none;
1487
- /* CSS3D owns card background/edge; passive overlay only contributes
1488
- readable text fragments and must never paint over CSS3D node chrome. */
1487
+ /* Passive overlay paints screen-space chrome for text-like nodes so the
1488
+ user does not see CSS3D's camera-scaled hard card edge in NEAR. */
1489
1489
  contain: layout style;
1490
1490
  overflow: visible;
1491
- background: transparent !important;
1492
- background-color: transparent !important;
1491
+ background: rgba(255, 255, 255, 0.985) !important;
1492
+ background-color: rgba(255, 255, 255, 0.985) !important;
1493
1493
  background-image: none !important;
1494
+ border: 1px solid rgba(55, 65, 81, 0.28) !important;
1495
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05) !important;
1496
+ border-radius: 2px !important;
1497
+ outline: none !important;
1498
+ }
1499
+
1500
+ .mind-map-text-overlay-v2-card.is-passive[data-content-type="memo"],
1501
+ .mind-map-text-overlay-v2-card.is-passive[data-content-type="note"] {
1502
+ border-color: rgba(55, 65, 81, 0.18) !important;
1503
+ border-radius: 6px !important;
1504
+ }
1505
+
1506
+ .mind-map-text-overlay-v2-card.is-passive[data-content-type="code"],
1507
+ .mind-map-text-overlay-v2-card.is-passive[data-content-type="text"],
1508
+ .mind-map-text-overlay-v2-card.is-passive[data-content-type="markdown"] {
1509
+ border-radius: 1px !important;
1494
1510
  }
1495
1511
 
1496
1512
  .mind-map-text-overlay-v2-card.is-passive .mind-map-text-overlay-v2-body {
1497
1513
  pointer-events: auto;
1514
+ text-rendering: auto;
1515
+ -webkit-font-smoothing: auto;
1516
+ -moz-osx-font-smoothing: auto;
1498
1517
  }
1499
1518
 
1500
1519
  .mind-map-text-overlay-v2-shell {
@@ -2042,6 +2061,20 @@ html.mindcanvas-platform-apple .css3d-resolution-wrapper.is-automation-relation-
2042
2061
  caret-color: transparent !important;
2043
2062
  }
2044
2063
 
2064
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed),
2065
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed) > .map-node,
2066
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed) > .map-node-bubble,
2067
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed) > .map-node-note,
2068
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed) > .map-node-code,
2069
+ .css3d-resolution-wrapper.mind-map-v2-source-suspended:not(.node-type-image):not(.node-type-video):not(.node-type-embed) > .map-node-memo:not(.map-node-agent):not(.map-node-automation) {
2070
+ background: transparent !important;
2071
+ background-color: transparent !important;
2072
+ background-image: none !important;
2073
+ border-color: transparent !important;
2074
+ outline-color: transparent !important;
2075
+ box-shadow: none !important;
2076
+ }
2077
+
2045
2078
  .css3d-resolution-wrapper.node-type-image .mind-map-v2-source-suspended,
2046
2079
  .css3d-resolution-wrapper.node-type-video .mind-map-v2-source-suspended,
2047
2080
  .css3d-resolution-wrapper.node-type-embed .mind-map-v2-source-suspended,
@@ -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 = '20260621-video-prefetch-v785';
8
+ const MINDMAP_CORE_BUILD_ID = '20260621-asset-resolve-overlay-v793';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -77,7 +77,7 @@
77
77
  });
78
78
  const CanvasPhaseSet = new Set(Object.values(CanvasPhase));
79
79
  const VIEWPORT_METRICS_POLL_INTERVAL = 480;
80
- const PASSIVE_DOM_OVERLAY_RUNTIME_ENABLED = false;
80
+ const PASSIVE_DOM_OVERLAY_RUNTIME_ENABLED = true;
81
81
  const PASSIVE_OVERLAY_ZOOM_DEFER_VISIBLE_THRESHOLD = 24;
82
82
  const PASSIVE_OVERLAY_INTERACTIVE_REFRESH_INTERVAL_MS = 48;
83
83
  const PASSIVE_OVERLAY_INTERACTIVE_DENSE_REFRESH_INTERVAL_MS = 72;
@@ -1131,6 +1131,7 @@
1131
1131
  enableDomOverlay: true,
1132
1132
  enablePassiveOverlayForVisibleNodes: true,
1133
1133
  enableOverlayCssZoom: true,
1134
+ enableSharedNodeViewOverlay: true,
1134
1135
  enableVisibilityCulling: true,
1135
1136
  enableLodUpdate: true,
1136
1137
  enableWebglRender: true,
@@ -9023,6 +9024,16 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
9023
9024
  const overlayV2State = useTextOverlayV2 && shouldTrackTextOverlayDirty
9024
9025
  ? (this._textOverlayV2State || null)
9025
9026
  : null;
9027
+ let hasVisibleOverlayV2Card = false;
9028
+ if (overlayV2State?.cards?.forEach) {
9029
+ overlayV2State.cards.forEach(card => {
9030
+ if (hasVisibleOverlayV2Card || !card || card.style.display === 'none') {
9031
+ return;
9032
+ }
9033
+
9034
+ hasVisibleOverlayV2Card = true;
9035
+ });
9036
+ }
9026
9037
  const hasOverlayV2DirtyState = !!(
9027
9038
  overlayV2State && (
9028
9039
  overlayV2State.dirtyLayout === true ||
@@ -9033,6 +9044,12 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
9033
9044
  );
9034
9045
  const hasOverlayFocus = hasFocusedTextOverlay || hasEditingOverlay;
9035
9046
  const hasMotionBlockingOverlayFocus = hasEditingOverlay;
9047
+ const shouldCleanupInactivePassiveOverlay =
9048
+ useTextOverlayV2 &&
9049
+ hasVisibleOverlayV2Card === true &&
9050
+ shouldRenderPassiveDomTextOverlay !== true &&
9051
+ hasLiveInteractiveDomOverlay !== true &&
9052
+ !hasOverlayFocus;
9036
9053
  const visibleOverlayCount = isPassiveDomOverlayRuntimeEnabled
9037
9054
  ? Math.max(
9038
9055
  Number(this._visibleIds?.size || 0),
@@ -9563,7 +9580,7 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
9563
9580
  const shouldSyncTextOverlays =
9564
9581
  effectiveOverlayDirty === true ||
9565
9582
  (useTextOverlayV2
9566
- ? (hasOverlayV2DirtyState || shouldRefreshPassiveDomTextOverlay || shouldFinalizePassiveOverlayLayout || (hasMotionBlockingOverlayFocus && shouldUpdateCss3d))
9583
+ ? (hasOverlayV2DirtyState || shouldCleanupInactivePassiveOverlay || shouldRefreshPassiveDomTextOverlay || shouldFinalizePassiveOverlayLayout || (hasMotionBlockingOverlayFocus && shouldUpdateCss3d))
9567
9584
  : (hasMotionBlockingOverlayFocus && shouldUpdateCss3d));
9568
9585
  const shouldAttemptPassiveOverlayCameraFastPath =
9569
9586
  useTextOverlayV2 &&
@@ -27,14 +27,21 @@
27
27
  let _cssImagePromotionTimer = 0;
28
28
  let _cssImageBlobUrlCache = new Map();
29
29
  let _cssImageBlobUrlPending = new Map();
30
+ let _cssImageMissingLoopbackUrls = new Set();
30
31
  let _cssVideoBlobUrlCache = new Map();
31
32
  let _cssVideoBlobUrlPending = new Map();
33
+ let _cssVideoMissingLoopbackUrls = new Set();
34
+ let _cssLoopbackAssetResolveCache = new Map();
35
+ let _cssLoopbackAssetResolvePending = new Map();
32
36
  let _cssImageDisplayQueue = [];
33
37
  let _cssImageDisplayActiveCount = 0;
34
38
  let _cssImageDisplayPumpScheduled = false;
35
39
  const CSS_VIDEO_PROXY_MAX_DEVICE_PIXEL_RATIO = 2;
36
40
  const VIDEO_PLAYBACK_INTENT_METADATA_KEY = 'VideoPlaybackIntent';
37
41
  const CSV_TABLE_CONTENT_TYPE = 'csv-table';
42
+ const NODE_VIEW_HOST_CSS3D = 'css3d';
43
+ const NODE_VIEW_HOST_OVERLAY = 'overlay';
44
+ const NODE_VIEW_RENDERER_VERSION = 'shared-node-view-v1';
38
45
  const CSV_TABLE_MAX_RENDER_ROWS = 500;
39
46
  const CSV_TABLE_MAX_RENDER_COLUMNS = 80;
40
47
  const REMOTE_FLEET_DISPLAY_NAME = 'Multi Desktop Monitor';
@@ -1653,6 +1660,92 @@
1653
1660
  return LOOPBACK_ASSET_URL_REGEX.test(String(url || '').trim());
1654
1661
  }
1655
1662
 
1663
+ function getCssLoopbackAssetResolveUrl(targetUrl) {
1664
+ const normalized = normalizeCssMediaUrl(targetUrl);
1665
+ if (!normalized || !isLoopbackCssAssetUrl(normalized)) {
1666
+ return '';
1667
+ }
1668
+
1669
+ try {
1670
+ const currentOrigin = window?.location?.origin || '';
1671
+ if (!currentOrigin) {
1672
+ return '';
1673
+ }
1674
+
1675
+ const target = new URL(normalized, window?.location?.href || currentOrigin);
1676
+ return `${currentOrigin}/api/assets/resolve?path=${encodeURIComponent(target.pathname)}`;
1677
+ } catch {
1678
+ return '';
1679
+ }
1680
+ }
1681
+
1682
+ async function resolveLoopbackCssAssetUrl(targetUrl, mediaKind = 'asset') {
1683
+ const normalized = normalizeCssMediaUrl(targetUrl);
1684
+ if (!normalized || !isLoopbackCssAssetUrl(normalized)) {
1685
+ return normalized;
1686
+ }
1687
+
1688
+ const cached = _cssLoopbackAssetResolveCache.get(normalized);
1689
+ if (cached) {
1690
+ if (cached.exists === false) {
1691
+ throw new Error(`Loopback ${mediaKind} asset was already reported missing`);
1692
+ }
1693
+ return normalizeCssMediaUrl(cached.url || normalized) || normalized;
1694
+ }
1695
+
1696
+ const existingPending = _cssLoopbackAssetResolvePending.get(normalized);
1697
+ if (existingPending) {
1698
+ return await existingPending;
1699
+ }
1700
+
1701
+ const resolveUrl = getCssLoopbackAssetResolveUrl(normalized);
1702
+ if (!resolveUrl) {
1703
+ return normalized;
1704
+ }
1705
+
1706
+ const pending = (async () => {
1707
+ const response = await fetch(resolveUrl, {
1708
+ cache: 'no-store',
1709
+ targetAddressSpace: 'loopback'
1710
+ });
1711
+
1712
+ if (response.status === 404) {
1713
+ // Older LocalBridge builds do not expose the resolver. Keep the
1714
+ // previous direct asset path so existing installations still work.
1715
+ return normalized;
1716
+ }
1717
+
1718
+ if (!response.ok) {
1719
+ throw new Error(`Asset resolve HTTP ${response.status}`);
1720
+ }
1721
+
1722
+ const payload = await response.json().catch(() => null);
1723
+ if (!payload || payload.exists !== true) {
1724
+ _cssLoopbackAssetResolveCache.set(normalized, {
1725
+ exists: false,
1726
+ url: normalized
1727
+ });
1728
+ throw new Error(`Loopback ${mediaKind} asset is missing`);
1729
+ }
1730
+
1731
+ const resolvedUrl = normalizeCssMediaUrl(payload.url || normalized) || normalized;
1732
+ _cssLoopbackAssetResolveCache.set(normalized, {
1733
+ exists: true,
1734
+ url: resolvedUrl
1735
+ });
1736
+ return resolvedUrl;
1737
+ })();
1738
+
1739
+ _cssLoopbackAssetResolvePending.set(normalized, pending);
1740
+ try {
1741
+ return await pending;
1742
+ } finally {
1743
+ if (_cssLoopbackAssetResolvePending.get(normalized) === pending) {
1744
+ _cssLoopbackAssetResolvePending.delete(normalized);
1745
+ }
1746
+ }
1747
+ }
1748
+
1656
1749
  function shouldAttachCssMediaAuthorization(targetUrl, authToken) {
1657
1750
  const token = typeof authToken === 'string' ? authToken.trim() : '';
1658
1751
  if (!token) {
@@ -3565,6 +3658,10 @@
3565
3658
  return normalized;
3566
3659
  }
3567
3660
 
3661
+ if (_cssImageMissingLoopbackUrls.has(normalized)) {
3662
+ throw new Error('Loopback image asset was already reported missing');
3663
+ }
3664
+
3568
3665
  const cached = _cssImageBlobUrlCache.get(normalized);
3569
3666
  if (cached?.objectUrl) {
3570
3667
  touchCssImageBlobCacheEntry(normalized, cached);
@@ -3577,8 +3674,19 @@
3577
3674
  }
3578
3675
 
3579
3676
  const pendingFetch = (async () => {
3580
- const response = await fetch(normalized, getCssMediaFetchOptions(normalized));
3677
+ let resolvedAssetUrl = normalized;
3678
+ try {
3679
+ resolvedAssetUrl = await resolveLoopbackCssAssetUrl(normalized, 'image');
3680
+ } catch (error) {
3681
+ _cssImageMissingLoopbackUrls.add(normalized);
3682
+ throw error;
3683
+ }
3684
+
3685
+ const response = await fetch(resolvedAssetUrl, getCssMediaFetchOptions(resolvedAssetUrl));
3581
3686
  if (!response.ok) {
3687
+ if (response.status === 404) {
3688
+ _cssImageMissingLoopbackUrls.add(normalized);
3689
+ }
3582
3690
  throw new Error(`HTTP ${response.status}`);
3583
3691
  }
3584
3692
 
@@ -3622,6 +3730,10 @@
3622
3730
  return normalized;
3623
3731
  }
3624
3732
 
3733
+ if (_cssVideoMissingLoopbackUrls.has(normalized)) {
3734
+ throw new Error('Loopback video asset was already reported missing');
3735
+ }
3736
+
3625
3737
  const cached = _cssVideoBlobUrlCache.get(normalized);
3626
3738
  if (cached?.objectUrl) {
3627
3739
  touchCssVideoBlobCacheEntry(normalized, cached);
@@ -3634,8 +3746,19 @@
3634
3746
  }
3635
3747
 
3636
3748
  const pendingFetch = (async () => {
3637
- const response = await fetch(normalized, getCssMediaFetchOptions(normalized));
3749
+ let resolvedAssetUrl = normalized;
3750
+ try {
3751
+ resolvedAssetUrl = await resolveLoopbackCssAssetUrl(normalized, 'video');
3752
+ } catch (error) {
3753
+ _cssVideoMissingLoopbackUrls.add(normalized);
3754
+ throw error;
3755
+ }
3756
+
3757
+ const response = await fetch(resolvedAssetUrl, getCssMediaFetchOptions(resolvedAssetUrl));
3638
3758
  if (!response.ok) {
3759
+ if (response.status === 404) {
3760
+ _cssVideoMissingLoopbackUrls.add(normalized);
3761
+ }
3639
3762
  throw new Error(`HTTP ${response.status}`);
3640
3763
  }
3641
3764
 
@@ -3726,6 +3849,16 @@
3726
3849
  return false;
3727
3850
  }
3728
3851
 
3852
+ if (isLoopbackCssAssetUrl(normalized)) {
3853
+ mediaEl.dataset.resolvedMediaSourceUrl = '';
3854
+ mediaEl.removeAttribute('src');
3855
+ if (mediaEl.tagName === 'IMG') {
3856
+ mediaEl.dataset.mediaReady = '0';
3857
+ mediaEl.style.opacity = '0';
3858
+ }
3859
+ return false;
3860
+ }
3861
+
3729
3862
  const currentDisplaySource = String(mediaEl.getAttribute('src') || '').trim();
3730
3863
  if (currentDisplaySource !== normalized) {
3731
3864
  mediaEl.setAttribute('src', normalized);
@@ -23061,6 +23194,136 @@
23061
23194
  }
23062
23195
  // ▲▲▲ [New] ▲▲▲
23063
23196
 
23197
+ function getNodeViewContentInfo(nodeModel) {
23198
+ const contentTypeLower = String(nodeModel?.contentType ?? nodeModel?.ContentType ?? '').toLowerCase();
23199
+ const remoteFleetMonitor = isRemoteFleetMonitorNode(nodeModel);
23200
+ const visualContentTypeLower = remoteFleetMonitor ? 'templatelauncher' : contentTypeLower;
23201
+ const isMediaNode = contentTypeLower === 'image' || contentTypeLower === 'video' || contentTypeLower === 'embed';
23202
+
23203
+ return {
23204
+ contentTypeLower,
23205
+ remoteFleetMonitor,
23206
+ visualContentTypeLower,
23207
+ isMediaNode
23208
+ };
23209
+ }
23210
+
23211
+ function shouldCreateDynamicNodeView(nodeModel, info = getNodeViewContentInfo(nodeModel)) {
23212
+ if (info.remoteFleetMonitor) {
23213
+ return true;
23214
+ }
23215
+
23216
+ const dynamicTypes = ['text', 'markdown', 'code', 'note', 'memo', CSV_TABLE_CONTENT_TYPE, 'templatelauncher', 'image', 'video', 'embed'];
23217
+ return dynamicTypes.includes(info.contentTypeLower);
23218
+ }
23219
+
23220
+ function shouldBypassNodeViewTemplate(nodeModel, info = getNodeViewContentInfo(nodeModel)) {
23221
+ if (info.remoteFleetMonitor) {
23222
+ return true;
23223
+ }
23224
+
23225
+ return info.contentTypeLower === 'text'
23226
+ || info.contentTypeLower === 'markdown'
23227
+ || info.contentTypeLower === 'note'
23228
+ || info.contentTypeLower === 'memo'
23229
+ || info.contentTypeLower === CSV_TABLE_CONTENT_TYPE
23230
+ || info.contentTypeLower === 'templatelauncher'
23231
+ || info.contentTypeLower === 'image'
23232
+ || info.contentTypeLower === 'video'
23233
+ || info.contentTypeLower === 'embed';
23234
+ }
23235
+
23236
+ function createNodeViewElement(module, nodeModel, options = {}) {
23237
+ if (module) {
23238
+ _module = module;
23239
+ }
23240
+
23241
+ const nodeId = getNodeId(nodeModel);
23242
+ if (!nodeId) {
23243
+ return null;
23244
+ }
23245
+
23246
+ const hostKind = String(options.hostKind || NODE_VIEW_HOST_CSS3D).trim().toLowerCase() || NODE_VIEW_HOST_CSS3D;
23247
+ const info = getNodeViewContentInfo(nodeModel);
23248
+ const templateId = `node-${nodeId}`;
23249
+ let templateElement = document.getElementById(templateId);
23250
+
23251
+ if (shouldBypassNodeViewTemplate(nodeModel, info)) {
23252
+ templateElement = null;
23253
+ }
23254
+
23255
+ let isDynamicallyCreated = false;
23256
+ if (!templateElement) {
23257
+ if (shouldCreateDynamicNodeView(nodeModel, info)) {
23258
+ log(`[MindMapCss3DManager] Creating shared NodeView DOM for ${info.visualContentTypeLower || info.contentTypeLower} node ${nodeId} (${hostKind})`);
23259
+ templateElement = createDynamicNodeElement(nodeModel);
23260
+ isDynamicallyCreated = true;
23261
+ } else {
23262
+ if (!window._templateWarningCount) window._templateWarningCount = 0;
23263
+ if (window._templateWarningCount < 5) {
23264
+ warn(`[MindMapCss3DManager] Template not found for node ${nodeId} (async templates pending). Template ID: ${templateId}`);
23265
+ window._templateWarningCount++;
23266
+ if (window._templateWarningCount === 5) {
23267
+ warn('[MindMapCss3DManager] ... suppressing further template warnings. Templates will be available after async render.');
23268
+ }
23269
+ }
23270
+ return null;
23271
+ }
23272
+ }
23273
+
23274
+ const width = Number(nodeModel.width || nodeModel.Width || 400);
23275
+ const height = Number(nodeModel.height || nodeModel.Height || 200);
23276
+ const allowsExternalNodeChrome = allowsNodeExternalChrome(nodeModel);
23277
+ const allowsExternalMemoChrome =
23278
+ info.contentTypeLower === 'memo'
23279
+ && allowsExternalNodeChrome;
23280
+ const element = isDynamicallyCreated
23281
+ ? templateElement
23282
+ : templateElement.cloneNode(true);
23283
+ const idPrefix = String(options.idPrefix || (hostKind === NODE_VIEW_HOST_OVERLAY ? 'node-view-overlay' : 'css3d-node')).trim();
23284
+ const resolutionScale = Math.max(1, Number(options.resolutionScale || getCss3dNodeResolutionScale(info.contentTypeLower) || 1));
23285
+
23286
+ element.id = `${idPrefix}-${nodeId}`;
23287
+ element.dataset.nodeId = nodeId;
23288
+ element.dataset.nodeViewHost = hostKind;
23289
+ element.dataset.nodeViewRenderer = NODE_VIEW_RENDERER_VERSION;
23290
+ element.dataset.contentType = info.visualContentTypeLower || info.contentTypeLower || '';
23291
+ element.style.overflow = allowsExternalNodeChrome ? 'visible' : 'hidden';
23292
+ if (allowsExternalNodeChrome) {
23293
+ element.style.contain = 'layout style';
23294
+ element.style.contentVisibility = 'visible';
23295
+ element.style.containIntrinsicSize = 'auto';
23296
+ }
23297
+ element.style.display = '';
23298
+ element.style.transformOrigin = '0% 0%';
23299
+ applyCss3dResolutionLayout(element, width, height, resolutionScale, {
23300
+ resetLayoutTransform: true
23301
+ });
23302
+ element.style.borderRadius = '0px';
23303
+ element.style.position = 'absolute';
23304
+ element.style.left = '0px';
23305
+ element.style.top = '0px';
23306
+ element.style.transition = 'none';
23307
+ element.style.webkitTransition = 'none';
23308
+ element.style.willChange = 'auto';
23309
+ element.style.backfaceVisibility = 'visible';
23310
+
23311
+ return {
23312
+ element,
23313
+ width,
23314
+ height,
23315
+ contentTypeLower: info.contentTypeLower,
23316
+ visualContentTypeLower: info.visualContentTypeLower,
23317
+ isMediaNode: info.isMediaNode,
23318
+ isDynamicallyCreated,
23319
+ allowsExternalNodeChrome,
23320
+ allowsExternalMemoChrome,
23321
+ resolutionScale,
23322
+ hostKind,
23323
+ rendererVersion: NODE_VIEW_RENDERER_VERSION
23324
+ };
23325
+ }
23326
+
23064
23327
  function appendCss3dResizeHitZones(wrapper, nodeModel) {
23065
23328
  if (!(wrapper instanceof HTMLElement) || !nodeModel?.id) {
23066
23329
  return;
@@ -23134,7 +23397,8 @@
23134
23397
  return null;
23135
23398
  }
23136
23399
 
23137
- const templateId = `node-${nodeModel.id}`;
23400
+ const resolvedNodeId = getNodeId(nodeModel);
23401
+ const templateId = `node-${resolvedNodeId}`;
23138
23402
  let templateElement = document.getElementById(templateId);
23139
23403
 
23140
23404
  // ▼▼▼ [New] 템플릿이 없으면 동적으로 생성 (text/markdown 타입용) ▼▼▼
@@ -23164,14 +23428,14 @@
23164
23428
  const dynamicTypes = ['text', 'markdown', 'code', 'note', 'memo', CSV_TABLE_CONTENT_TYPE, 'templatelauncher', 'image', 'video', 'embed'];
23165
23429
 
23166
23430
  if (remoteFleetMonitor || dynamicTypes.includes(contentTypeLower)) {
23167
- log(`[MindMapCss3DManager] Creating dynamic DOM element for ${visualContentTypeLower || contentTypeLower} node ${nodeModel.id}`);
23168
- templateElement = createDynamicNodeElement(nodeModel);
23431
+ log(`[MindMapCss3DManager] Deferring dynamic DOM element for ${visualContentTypeLower || contentTypeLower} node ${resolvedNodeId} to shared NodeView`);
23432
+ templateElement = document.createElement('div');
23169
23433
  isDynamicallyCreated = true;
23170
23434
  } else {
23171
23435
  // 경고 로그 제한 (초기 로딩 시 템플릿이 아직 렌더링되지 않은 경우 스팸 방지)
23172
23436
  if (!window._templateWarningCount) window._templateWarningCount = 0;
23173
23437
  if (window._templateWarningCount < 5) {
23174
- warn(`[MindMapCss3DManager] Template not found for node ${nodeModel.id} (async templates pending). Template ID: ${templateId}`);
23438
+ warn(`[MindMapCss3DManager] Template not found for node ${resolvedNodeId} (async templates pending). Template ID: ${templateId}`);
23175
23439
  window._templateWarningCount++;
23176
23440
  if (window._templateWarningCount === 5) {
23177
23441
  warn(`[MindMapCss3DManager] ... suppressing further template warnings. Templates will be available after async render.`);
@@ -23182,11 +23446,11 @@
23182
23446
  }
23183
23447
  // ▲▲▲ [New] ▲▲▲
23184
23448
 
23185
- const existingCss3dId = `css3d-node-${nodeModel.id}`;
23186
- const nodeEntry = module.nodeObjectsById.get(nodeModel.id);
23449
+ const existingCss3dId = `css3d-node-${resolvedNodeId}`;
23450
+ const nodeEntry = module.nodeObjectsById.get(resolvedNodeId);
23187
23451
 
23188
23452
  if (nodeEntry && nodeEntry.cssObject) {
23189
- warn(`[MindMapCss3DManager] CSS3D object already exists in memory for node ${nodeModel.id}. Returning existing object.`);
23453
+ warn(`[MindMapCss3DManager] CSS3D object already exists in memory for node ${resolvedNodeId}. Returning existing object.`);
23190
23454
  return nodeEntry.cssObject;
23191
23455
  }
23192
23456
 
@@ -23210,18 +23474,22 @@
23210
23474
  */
23211
23475
  // ▲▲▲ [Critical fix] ▲▲▲
23212
23476
 
23213
- const width = Number(nodeModel.width || 400);
23214
- const height = Number(nodeModel.height || 200);
23215
- const isMediaNode = contentTypeLower === 'image' || contentTypeLower === 'video' || contentTypeLower === 'embed';
23216
- const allowsExternalNodeChrome = allowsNodeExternalChrome(nodeModel);
23217
- const allowsExternalMemoChrome =
23218
- contentTypeLower === 'memo'
23219
- && allowsExternalNodeChrome;
23220
- const clonedElement = isDynamicallyCreated
23221
- ? templateElement
23222
- : templateElement.cloneNode(true);
23223
- clonedElement.id = `css3d-node-${nodeModel.id}`;
23224
- clonedElement.dataset.nodeId = nodeModel.id;
23477
+ const nodeView = createNodeViewElement(module, nodeModel, {
23478
+ hostKind: NODE_VIEW_HOST_CSS3D,
23479
+ idPrefix: 'css3d-node'
23480
+ });
23481
+ if (!nodeView?.element) {
23482
+ return null;
23483
+ }
23484
+
23485
+ const width = nodeView.width;
23486
+ const height = nodeView.height;
23487
+ const isMediaNode = nodeView.isMediaNode;
23488
+ const allowsExternalNodeChrome = nodeView.allowsExternalNodeChrome;
23489
+ const allowsExternalMemoChrome = nodeView.allowsExternalMemoChrome;
23490
+ const clonedElement = nodeView.element;
23491
+ clonedElement.id = `css3d-node-${resolvedNodeId}`;
23492
+ clonedElement.dataset.nodeId = resolvedNodeId;
23225
23493
  clonedElement.style.overflow = allowsExternalNodeChrome ? 'visible' : 'hidden';
23226
23494
  if (allowsExternalNodeChrome) {
23227
23495
  clonedElement.style.contain = 'layout style';
@@ -23232,7 +23500,7 @@
23232
23500
  clonedElement.style.transformOrigin = '0% 0%';
23233
23501
 
23234
23502
  // ▼▼▼ [Clarity] Scale factor for high-resolution rendering ▼▼▼
23235
- const resolutionScale = getCss3dNodeResolutionScale(contentTypeLower);
23503
+ const resolutionScale = nodeView.resolutionScale;
23236
23504
  applyCss3dResolutionLayout(clonedElement, width, height, resolutionScale, {
23237
23505
  resetLayoutTransform: true
23238
23506
  });
@@ -23265,7 +23533,7 @@
23265
23533
  wrapper.style.minHeight = `${height * resolutionScale}px`;
23266
23534
  wrapper.style.maxWidth = `${width * resolutionScale}px`;
23267
23535
  wrapper.style.maxHeight = `${height * resolutionScale}px`;
23268
- wrapper.dataset.nodeId = nodeModel.id;
23536
+ wrapper.dataset.nodeId = resolvedNodeId;
23269
23537
  wrapper.style.transition = CSS3D_WRAPPER_TRANSITION;
23270
23538
  wrapper.style.webkitTransition = CSS3D_WRAPPER_WEBKIT_TRANSITION;
23271
23539
 
@@ -23369,7 +23637,7 @@
23369
23637
  const css3dObj = new CSS3DObjectCtor(wrapper);
23370
23638
  css3dObj.scale.set(1 / resolutionScale, 1 / resolutionScale, 1 / resolutionScale);
23371
23639
  css3dObj.visible = false;
23372
- css3dObj.userData.nodeId = nodeModel.id;
23640
+ css3dObj.userData.nodeId = resolvedNodeId;
23373
23641
  css3dObj.userData.worldWidth = width;
23374
23642
  css3dObj.userData.worldHeight = height;
23375
23643
  css3dObj.userData.resolutionScale = resolutionScale;
@@ -23387,7 +23655,7 @@
23387
23655
  // ▲▲▲ [Fix] ▲▲▲
23388
23656
 
23389
23657
  requestAnimationFrame(() => {
23390
- syncCss3dScrollFromModel(module, nodeModel.id, {
23658
+ syncCss3dScrollFromModel(module, resolvedNodeId, {
23391
23659
  source: 'createCss3dObject'
23392
23660
  });
23393
23661
  });
@@ -23728,7 +23996,7 @@
23728
23996
  void resolveLocalCssMediaSource(mediaEl, nodeModel, contentTypeLower);
23729
23997
  } else {
23730
23998
  delete mediaEl.dataset.videoCanvasProxyFailed;
23731
- mediaEl.src = content;
23999
+ void setCssVideoElementSource(mediaEl, nodeModel, content, { load: true });
23732
24000
  }
23733
24001
  if (contentTypeLower === 'video' &&
23734
24002
  mediaEl.dataset?.lodMediaSuspended !== 'true' &&
@@ -24178,6 +24446,7 @@
24178
24446
  clearSelectableTextOverlay: clearSelectableTextOverlay,
24179
24447
  clearNativeTextSelectionSource: clearNativeTextSelectionSource,
24180
24448
  createCss3dObject: createCss3dObject,
24449
+ createNodeViewElement: createNodeViewElement,
24181
24450
  createEditingOverlayContent: createEditingOverlayContent,
24182
24451
  clearEditingOverlay: clearEditingOverlay,
24183
24452
  getTextInteractionContentSelectors: getTextInteractionContentSelectors,
@@ -51,7 +51,7 @@
51
51
  // ▲▲▲ [Usage] ▲▲▲
52
52
 
53
53
  // Procedural line rendering settings
54
- const LOD_RENDERER_BUILD_ID = '20260621-video-prefetch-v785';
54
+ const LOD_RENDERER_BUILD_ID = '20260621-asset-resolve-overlay-v793';
55
55
  const DirtyKind = Object.freeze({
56
56
  ResidentFullRebuild: 'resident-full-rebuild',
57
57
  ResidentPatch: 'resident-patch',
@@ -4482,13 +4482,41 @@ const LOD_RENDERER_BUILD_ID = '20260621-video-prefetch-v785';
4482
4482
  }
4483
4483
  }
4484
4484
 
4485
+ _restoreVisibleCss3dHighDetailForNear(nodeObjectsById, module, lodBand) {
4486
+ if (lodBand !== 'NEAR' || !nodeObjectsById) {
4487
+ return 0;
4488
+ }
4489
+
4490
+ let restoredCount = 0;
4491
+ for (const entry of nodeObjectsById.values()) {
4492
+ const cssElement = entry?.cssObject?.element || null;
4493
+ if (!cssElement ||
4494
+ entry.cssObject?.visible !== true ||
4495
+ cssElement.style.display === 'none' ||
4496
+ !cssElement.classList?.contains?.('lod-low')) {
4497
+ continue;
4498
+ }
4499
+
4500
+ this._setCss3dLodClass(entry, true);
4501
+ restoredCount++;
4502
+ }
4503
+
4504
+ if (restoredCount > 0 && module) {
4505
+ module._css3dVisualChangedThisFrame = true;
4506
+ module._css3dVisualChangedSinceLastRender = true;
4507
+ }
4508
+
4509
+ return restoredCount;
4510
+ }
4511
+
4485
4512
  _shouldUseHighCss3dDetail(entry, module) {
4486
4513
  const cameraZ = Number(module?.camera?.position?.z || 0);
4487
- const lodBand = this._currentLodBand || getLodBandForCameraZ(cameraZ);
4488
- if (lodBand === 'NEAR') {
4514
+ const cameraBand = getLodBandForCameraZ(cameraZ);
4515
+ if (cameraBand === 'NEAR') {
4489
4516
  return true;
4490
4517
  }
4491
4518
 
4519
+ const lodBand = this._currentLodBand || cameraBand;
4492
4520
  const nodeId = String(entry?.model?.id || entry?.model?.Id || entry?.glObject?.userData?.nodeId || '').trim();
4493
4521
  const selectedNodeId = String(module?.selectedNodeIdJs || module?.selectedNodeId || '').trim();
4494
4522
  if (nodeId && selectedNodeId && nodeId === selectedNodeId) {
@@ -6010,6 +6038,7 @@ const LOD_RENDERER_BUILD_ID = '20260621-video-prefetch-v785';
6010
6038
  // ▼▼▼ [Fix] Keep CSS3D visibility synced while panning in near mode ▼▼▼
6011
6039
  if (!this.isInLODMode) {
6012
6040
  this._currentLodBand = lodBand;
6041
+ this._restoreVisibleCss3dHighDetailForNear(nodeObjectsById, module, lodBand);
6013
6042
  const visibleIds = this._getNearVisibleIds(module);
6014
6043
  if (!visibleIds || visibleIds.size === 0) {
6015
6044
  perfTime('fullResHandoff', () => {
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const DEBUG = false;
5
- const PASSIVE_DOM_OVERLAY_ENABLED = false;
5
+ const PASSIVE_DOM_OVERLAY_ENABLED = true;
6
6
  const DEFAULT_OVERLAY_SCROLLABLE_SELECTOR = '.node-response, .note-content, .markdown-body, .prose, .note-textarea, .code-body, .code-content, .pdf-content, .text-content, .file-content, .map-node-memo__body, .map-node-memo__body-view, .map-node-memo__agent-plan-body, .map-node-memo__agent-console-body, pre, code';
7
7
  const PASSIVE_OVERLAY_NEAR_EXIT_GRACE_MS = 220;
8
8
  const PASSIVE_OVERLAY_CANDIDATE_GRACE_MS = 160;
@@ -336,7 +336,7 @@
336
336
  state.passiveNearStickyUntil = 0;
337
337
  }
338
338
 
339
- return hasVisibleLiveInteractiveOverlayCandidate(module);
339
+ return false;
340
340
  }
341
341
 
342
342
  function shouldUseVisiblePassiveOverlayCandidates(module) {
@@ -956,10 +956,6 @@
956
956
  return false;
957
957
  }
958
958
 
959
- if (entry && supportsTextOverlayEntry(entry)) {
960
- return false;
961
- }
962
-
963
959
  return true;
964
960
  }
965
961
 
@@ -1298,6 +1294,29 @@
1298
1294
  return Math.max(baseZIndex, candidate + baseZIndex);
1299
1295
  }
1300
1296
 
1297
+ function getDevicePixelRatio() {
1298
+ const dpr = Number(globalThis?.devicePixelRatio || 1);
1299
+ return Number.isFinite(dpr) && dpr > 0 ? Math.min(4, Math.max(1, dpr)) : 1;
1300
+ }
1301
+
1302
+ function snapScreenPixel(value, dpr = getDevicePixelRatio()) {
1303
+ const numeric = Number(value || 0);
1304
+ if (!Number.isFinite(numeric)) {
1305
+ return 0;
1306
+ }
1307
+
1308
+ return Math.round(numeric * dpr) / dpr;
1309
+ }
1310
+
1311
+ function snapScreenLength(value, dpr = getDevicePixelRatio()) {
1312
+ const numeric = Number(value || 0);
1313
+ if (!Number.isFinite(numeric)) {
1314
+ return 1;
1315
+ }
1316
+
1317
+ return Math.max(1 / dpr, Math.round(Math.max(1 / dpr, numeric) * dpr) / dpr);
1318
+ }
1319
+
1301
1320
  function applyPlacement(module, element, rect, zIndex, entry = null) {
1302
1321
  if (!module || !element || !rect) {
1303
1322
  return;
@@ -1305,8 +1324,13 @@
1305
1324
 
1306
1325
  const baseWidth = Math.max(1, Number(rect.baseWidth || rect.width || 1));
1307
1326
  const baseHeight = Math.max(1, Number(rect.baseHeight || rect.height || 1));
1308
- const scaleX = Number(rect.width || baseWidth) / baseWidth;
1309
- const scaleY = Number(rect.height || baseHeight) / baseHeight;
1327
+ const dpr = getDevicePixelRatio();
1328
+ const left = snapScreenPixel(rect.left || 0, dpr);
1329
+ const top = snapScreenPixel(rect.top || 0, dpr);
1330
+ const width = snapScreenLength(rect.width || baseWidth || 1, dpr);
1331
+ const height = snapScreenLength(rect.height || baseHeight || 1, dpr);
1332
+ const scaleX = width / baseWidth;
1333
+ const scaleY = height / baseHeight;
1310
1334
  const preferCssZoom = shouldPreferOverlayCssZoom(module, entry);
1311
1335
  const canUseZoom =
1312
1336
  preferCssZoom &&
@@ -1318,10 +1342,10 @@
1318
1342
  const shell = element.firstElementChild || null;
1319
1343
 
1320
1344
  element.style.display = 'block';
1321
- element.style.left = `${Number(rect.left || 0)}px`;
1322
- element.style.top = `${Number(rect.top || 0)}px`;
1323
- element.style.width = `${Math.max(1, Number(rect.width || baseWidth || 1))}px`;
1324
- element.style.height = `${Math.max(1, Number(rect.height || baseHeight || 1))}px`;
1345
+ element.style.left = `${left}px`;
1346
+ element.style.top = `${top}px`;
1347
+ element.style.width = `${width}px`;
1348
+ element.style.height = `${height}px`;
1325
1349
  element.style.transform = '';
1326
1350
  element.style.zoom = '';
1327
1351
  applyScrollbarScaleCompensation(element, scaleX, scaleY);
@@ -1336,14 +1360,18 @@
1336
1360
  applyScrollbarScaleCompensation(shell, scaleX, scaleY);
1337
1361
 
1338
1362
  if (canUseZoom) {
1339
- shell.style.zoom = `${scaleX}`;
1363
+ shell.style.zoom = `${Math.max(0.01, Math.round(scaleX * 100000) / 100000)}`;
1340
1364
  module._overlayDebugPlacementZoomCount = Number(module._overlayDebugPlacementZoomCount || 0) + 1;
1341
1365
  } else {
1342
- shell.style.transform = `scale(${scaleX}, ${scaleY})`;
1366
+ const snappedScaleX = Math.max(0.01, Math.round(scaleX * 100000) / 100000);
1367
+ const snappedScaleY = Math.max(0.01, Math.round(scaleY * 100000) / 100000);
1368
+ shell.style.transform = `scale(${snappedScaleX}, ${snappedScaleY})`;
1343
1369
  module._overlayDebugPlacementTransformCount = Number(module._overlayDebugPlacementTransformCount || 0) + 1;
1344
1370
  }
1345
1371
  } else if (!canUseZoom) {
1346
- element.style.transform = `scale(${scaleX}, ${scaleY})`;
1372
+ const snappedScaleX = Math.max(0.01, Math.round(scaleX * 100000) / 100000);
1373
+ const snappedScaleY = Math.max(0.01, Math.round(scaleY * 100000) / 100000);
1374
+ element.style.transform = `scale(${snappedScaleX}, ${snappedScaleY})`;
1347
1375
  module._overlayDebugPlacementTransformCount = Number(module._overlayDebugPlacementTransformCount || 0) + 1;
1348
1376
  }
1349
1377
  element.style.zIndex = String(resolveOverlayZIndex(entry, zIndex));
@@ -2823,6 +2851,77 @@
2823
2851
  return finalizeReadonlySourceClone(module, sourceRoot, preparedClone, type, interactive, { renderMode: renderMode });
2824
2852
  }
2825
2853
 
2854
+ function shouldUseSharedNodeViewOverlay(module, entry, mode) {
2855
+ if (module?.renderDebugFlags?.enableSharedNodeViewOverlay === false) {
2856
+ return false;
2857
+ }
2858
+
2859
+ const type = getContentType(entry);
2860
+ const normalizedMode = String(mode || '').trim().toLowerCase();
2861
+ return normalizedMode === 'full'
2862
+ || type === 'text'
2863
+ || type === 'markdown'
2864
+ || type === 'note'
2865
+ || type === 'code'
2866
+ || type === CSV_TABLE_CONTENT_TYPE;
2867
+ }
2868
+
2869
+ function createReadonlyOverlayFromNodeView(module, entry, options = {}) {
2870
+ const manager = getCss3dManager();
2871
+ const model = getModel(entry);
2872
+ if (!manager?.createNodeViewElement || !model) {
2873
+ return null;
2874
+ }
2875
+
2876
+ const sourceRoot = options.sourceRoot || getReadonlySelectionSourceRoot(entry);
2877
+ if (!sourceRoot) {
2878
+ return null;
2879
+ }
2880
+
2881
+ const type = getContentType(entry);
2882
+ const interactive = options.interactive === true;
2883
+ const renderMode = String(options.renderMode || getReadonlySelectionMode(entry) || type || '').trim().toLowerCase();
2884
+ if (!shouldUseSharedNodeViewOverlay(module, entry, renderMode)) {
2885
+ return null;
2886
+ }
2887
+
2888
+ const nodeView = manager.createNodeViewElement(module, model, {
2889
+ hostKind: 'overlay',
2890
+ idPrefix: 'node-view-overlay',
2891
+ readonly: true
2892
+ });
2893
+ const nodeViewRoot = nodeView?.element || null;
2894
+ if (!nodeViewRoot) {
2895
+ return null;
2896
+ }
2897
+
2898
+ nodeViewRoot.classList.add('mind-map-text-overlay-v2-node-view-source');
2899
+ nodeViewRoot.style.position = 'relative';
2900
+ nodeViewRoot.style.left = '0px';
2901
+ nodeViewRoot.style.top = '0px';
2902
+ nodeViewRoot.style.pointerEvents = 'none';
2903
+
2904
+ const preparedRoot = prepareOverlayClone(nodeViewRoot);
2905
+ if (!preparedRoot) {
2906
+ return null;
2907
+ }
2908
+
2909
+ const finalized = finalizeReadonlySourceClone(module, sourceRoot, preparedRoot, type, interactive, {
2910
+ renderMode: 'node-view'
2911
+ });
2912
+ if (!finalized?.container) {
2913
+ return null;
2914
+ }
2915
+
2916
+ finalized.container.dataset.overlaySource = 'node-view';
2917
+ finalized.container.dataset.nodeViewRenderer = nodeView.rendererVersion || 'shared-node-view-v1';
2918
+ return {
2919
+ ...finalized,
2920
+ sourceRoot,
2921
+ nodeViewSource: true
2922
+ };
2923
+ }
2924
+
2826
2925
  function getRelativeFragmentPlacement(hostElement, sourceElement, hostBaseWidth, hostBaseHeight) {
2827
2926
  if (!hostElement || !sourceElement) {
2828
2927
  return null;
@@ -3094,7 +3193,10 @@
3094
3193
  // Passive readonly overlays only need the text surface.
3095
3194
  // Keep card chrome/background/borders in CSS3D to avoid duplicate
3096
3195
  // painting and reduce DOM work during zoom/pan.
3097
- return createReadonlyContentOnlyShell(module, entry, options);
3196
+ return createReadonlyOverlayFromNodeView(module, entry, {
3197
+ ...options,
3198
+ renderMode: mode
3199
+ }) || createReadonlyContentOnlyShell(module, entry, options);
3098
3200
  }
3099
3201
 
3100
3202
  return null;
@@ -15,6 +15,8 @@
15
15
  const renderers = new Map();
16
16
  const failedImageAssetCache = new Map();
17
17
  const pendingImageAssetFetches = new Map();
18
+ const imageAssetResolveCache = new Map();
19
+ const pendingImageAssetResolves = new Map();
18
20
  let sharedMeasurer = null;
19
21
 
20
22
  function getSharedMeasurer() {
@@ -91,6 +93,99 @@
91
93
  .test(String(url || '').trim());
92
94
  }
93
95
 
96
+ function getImageAssetResolveUrl(targetUrl) {
97
+ const normalized = normalizeImageAssetUrl(targetUrl);
98
+ if (!normalized || !isLoopbackImageAssetUrl(normalized)) {
99
+ return '';
100
+ }
101
+
102
+ try {
103
+ const currentOrigin = window?.location?.origin || '';
104
+ if (!currentOrigin) {
105
+ return '';
106
+ }
107
+
108
+ const target = new URL(normalized, window?.location?.href || currentOrigin);
109
+ return `${currentOrigin}/api/assets/resolve?path=${encodeURIComponent(target.pathname)}`;
110
+ } catch {
111
+ return '';
112
+ }
113
+ }
114
+
115
+ async function resolveLoopbackImageAssetUrl(targetUrl) {
116
+ const normalized = normalizeImageAssetUrl(targetUrl);
117
+ if (!normalized || !isLoopbackImageAssetUrl(normalized)) {
118
+ return normalized;
119
+ }
120
+
121
+ const cached = imageAssetResolveCache.get(normalized);
122
+ if (cached) {
123
+ if (cached.exists === false) {
124
+ const error = new Error('Loopback image asset is missing');
125
+ error.status = 404;
126
+ error.url = normalized;
127
+ throw error;
128
+ }
129
+ return normalizeImageAssetUrl(cached.url || normalized) || normalized;
130
+ }
131
+
132
+ const existingPending = pendingImageAssetResolves.get(normalized);
133
+ if (existingPending) {
134
+ return await existingPending;
135
+ }
136
+
137
+ const resolveUrl = getImageAssetResolveUrl(normalized);
138
+ if (!resolveUrl) {
139
+ return normalized;
140
+ }
141
+
142
+ const pending = (async () => {
143
+ const response = await fetch(resolveUrl, {
144
+ cache: 'no-store',
145
+ targetAddressSpace: 'loopback'
146
+ });
147
+
148
+ if (response.status === 404) {
149
+ return normalized;
150
+ }
151
+
152
+ if (!response.ok) {
153
+ const error = new Error(`Asset resolve HTTP ${response.status}`);
154
+ error.status = response.status;
155
+ error.url = normalized;
156
+ throw error;
157
+ }
158
+
159
+ const payload = await response.json().catch(() => null);
160
+ if (!payload || payload.exists !== true) {
161
+ imageAssetResolveCache.set(normalized, {
162
+ exists: false,
163
+ url: normalized
164
+ });
165
+ const error = new Error('Loopback image asset is missing');
166
+ error.status = 404;
167
+ error.url = normalized;
168
+ throw error;
169
+ }
170
+
171
+ const resolvedUrl = normalizeImageAssetUrl(payload.url || normalized) || normalized;
172
+ imageAssetResolveCache.set(normalized, {
173
+ exists: true,
174
+ url: resolvedUrl
175
+ });
176
+ return resolvedUrl;
177
+ })();
178
+
179
+ pendingImageAssetResolves.set(normalized, pending);
180
+ try {
181
+ return await pending;
182
+ } finally {
183
+ if (pendingImageAssetResolves.get(normalized) === pending) {
184
+ pendingImageAssetResolves.delete(normalized);
185
+ }
186
+ }
187
+ }
188
+
94
189
  function shouldAttachImageAssetAuthorization(targetUrl, authToken) {
95
190
  const token = typeof authToken === 'string' ? authToken.trim() : '';
96
191
  if (!token) {
@@ -1839,7 +1934,10 @@
1839
1934
  return Object.keys(requestOptions).length > 0 ? requestOptions : undefined;
1840
1935
  };
1841
1936
 
1842
- const fetchWithAuth = (targetUrl) => fetch(targetUrl, getFetchOptions(targetUrl));
1937
+ const fetchWithAuth = async (targetUrl) => {
1938
+ const resolvedTargetUrl = await resolveLoopbackImageAssetUrl(targetUrl);
1939
+ return fetch(resolvedTargetUrl, getFetchOptions(resolvedTargetUrl));
1940
+ };
1843
1941
  const fetchBlobWithAuth = async (targetUrl) => {
1844
1942
  const requestKey = getImageAssetErrorCacheKey(targetUrl) || normalizeImageAssetUrl(targetUrl);
1845
1943
  if (!requestKey) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-YgJcRsTo1IPvOdBuYHz3Mkio500xFMYOisAGvpYEHik=",
4
+ "hash": "sha256-YIk9DX4DKiZTExQMveGB2um1rjSUq/+iCFtZzwItvUg=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -131,7 +131,7 @@
131
131
  "MindExecution.Plugins.Directory.ju87t8h3zs.dll": "MindExecution.Plugins.Directory.dll",
132
132
  "MindExecution.Plugins.PlanMaster.krn3jgvjpa.dll": "MindExecution.Plugins.PlanMaster.dll",
133
133
  "MindExecution.Plugins.YouTube.5m53srfud6.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.rv3rw2h14g.dll": "MindExecution.Shared.dll",
134
+ "MindExecution.Shared.74msadwmew.dll": "MindExecution.Shared.dll",
135
135
  "MindExecution.Web.82k6ktlkfg.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
@@ -283,7 +283,7 @@
283
283
  "MindExecution.Plugins.Business.nyr3v25v48.dll": "sha256-T4c7+fDE4TQAXAoFmHnYM+tT7RSvAM7W0VinLPoD+6U=",
284
284
  "MindExecution.Plugins.Concept.l9z9tx9svt.dll": "sha256-N/QoILmTilvX5Zo4SCy7ENTCqMzSC6RIEQlEwtxkGPo=",
285
285
  "MindExecution.Plugins.PlanMaster.krn3jgvjpa.dll": "sha256-8IFpm/2fpsWD3syyl58LTSWSGC8PfcrsLdmmSKR1Sq0=",
286
- "MindExecution.Shared.rv3rw2h14g.dll": "sha256-ZwT/+Wjs0ZoDe3XZwwDlkPwo7xexhoK1RcyI2rEiywY=",
286
+ "MindExecution.Shared.74msadwmew.dll": "sha256-93zoKe74n9ZSDo3yiXxg1tAOzOaCbuapATS84OHGoNU=",
287
287
  "MindExecution.Web.82k6ktlkfg.dll": "sha256-ECPSyJrziGEVhCw8ZUXkRsfTDjWCk676m5yz99b13N4="
288
288
  },
289
289
  "lazyAssembly": {
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Business Execution OS for solo builders</title>
8
8
  <meta name="description" content="MindExec is an AI business execution OS for solo builders who want to turn notes, research, assets, and repeatable execution Skills into revenue-producing work." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260621-video-prefetch-v785" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260621-video-prefetch-v785" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260621-asset-resolve-overlay-v793" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260621-asset-resolve-overlay-v793" />
12
12
  <!-- ??좎뜦堉??Font Awesome (local) ??좎뜦堉??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ??좎뜦堉??-->
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260621-video-prefetch-v785';
582
+ const scriptVersion = '20260621-asset-resolve-overlay-v793';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "jBFdtOmP",
2
+ "version": "dFWDosxV",
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-QyE+9OXJBCXf5eXHuMaxnGjVAMIuQr72tNhfSCk6nro=",
45
+ "hash": "sha256-ha65+2w0xEoB2Mo1nEHyp7RzEI399tRfJqk2UVmQ49w=",
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-bcZumly3UqFp3yXp94h51Uect2dFJXYGVRKxtJkh4HE=",
81
+ "hash": "sha256-F1INYfbVbuItU+haAmKD4slfzCEglpC7GqIV/cU0oEw=",
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-1hnC4MKgwN6Nsz5+5t3Kjy6m/3kfaI7QNMFKaS8H/hA=",
89
+ "hash": "sha256-vNc3xxIK/8spAKK8PmqcgHnFrXO/i2QWu1rC8MjdHgY=",
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-btpizB1MkhK5bRl7BmaL4+ZNHh2bZrg160eaiBbzRy8=",
117
+ "hash": "sha256-PzCyPbFR/WA4fkMLgladZPjcf5UoNR+APa6cSvkxa84=",
118
118
  "url": "_content/MindExecution.Shared/js/mind-map-lod-renderer.js"
119
119
  },
120
120
  {
@@ -154,11 +154,11 @@
154
154
  "url": "_content/MindExecution.Shared/js/mind-map-text-lod-system.js"
155
155
  },
156
156
  {
157
- "hash": "sha256-pEdLlTuNhb8coOCRcx1fX+xLyD8ATKxwR8CXupgbgtk=",
157
+ "hash": "sha256-+6Qe/8m04tQbsF7EQVM/v93sHAh6rjjN1btiNOe9EnQ=",
158
158
  "url": "_content/MindExecution.Shared/js/mind-map-text-overlay-v2.js"
159
159
  },
160
160
  {
161
- "hash": "sha256-rXMMiOLaRGM1Dzp+IuVTVGJIQCVrr8apnByC4RmpbIQ=",
161
+ "hash": "sha256-uWZUzao/aNpU3SD4jtCVFMrTaGYtCAOAO6lxNt5UL1g=",
162
162
  "url": "_content/MindExecution.Shared/js/mind-map-texture-factory.js"
163
163
  },
164
164
  {
@@ -442,8 +442,8 @@
442
442
  "url": "_framework/MindExecution.Plugins.YouTube.5m53srfud6.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-ZwT/+Wjs0ZoDe3XZwwDlkPwo7xexhoK1RcyI2rEiywY=",
446
- "url": "_framework/MindExecution.Shared.rv3rw2h14g.dll"
445
+ "hash": "sha256-93zoKe74n9ZSDo3yiXxg1tAOzOaCbuapATS84OHGoNU=",
446
+ "url": "_framework/MindExecution.Shared.74msadwmew.dll"
447
447
  },
448
448
  {
449
449
  "hash": "sha256-ECPSyJrziGEVhCw8ZUXkRsfTDjWCk676m5yz99b13N4=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-/y7nWgvIiQwrfUfw4xVQPgcy9m78NRMdMq8TbflUDDI=",
773
+ "hash": "sha256-nQRvRaM/ZyOgFFU3k4VFcn5RCxxhN4uAIHf5Y+jHaLI=",
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-mvQ5z9xIyVIoYzxOcaAL8lUhs30CGFe2NcbVq3Wa0AU=",
837
+ "hash": "sha256-ZabWv7pPxmz7GAn1Da2AWSY8HrUnVVLo9oSmmHDOG2c=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: jBFdtOmP */
1
+ /* Manifest version: dFWDosxV */
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