noteconnection 1.6.1 → 1.6.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
@@ -279,9 +279,26 @@ npm start -- --path "E:/Knowledge/ObsidianVault" --no-gpu
279
279
  Managing your knowledge base source is now easier than ever.
280
280
 
281
281
  - **First Run Setup**: On first launch, you will be prompted to select your `Knowledge_Base` folder.
282
- - **Persistent Config**: Your selection is saved in `kb_config.json` and remembered across restarts.
282
+ - **Persistent Config (`app_config.toml`)**: Your KB path, language, and multi-window preferences are saved in `%LOCALAPPDATA%/NoteConnection/app_config.toml` (Windows default) and remembered across restarts.
283
+ - **Legacy Auto-Migration**: If a legacy `kb_config.json` exists in the same config directory, NoteConnection automatically migrates it to `app_config.toml`.
283
284
  - **Change Anytime**: Use the **File > Change Knowledge Base...** menu option to switch folders instantly.
284
285
  - **Reset**: Use **File > Reset to Default** to return to the bundled demo notes.
286
+ - **Config Path Overrides**: Set `NOTE_CONNECTION_CONFIG_PATH` (full file path) or `NOTE_CONNECTION_CONFIG_DIR` (directory) to customize where `app_config.toml` is stored.
287
+ - **Window Behavior Tuning**: Edit `[multi_window]` in `app_config.toml` (`single_window_mode`, `hide_tauri_when_pathmode_opens`, `restore_tauri_when_pathmode_exits`, `confirm_before_full_shutdown_from_godot`, `sync_language`).
288
+ - **Detailed Config Guide**: See [`docs/en/app_config.toml_guide.md`](docs/en/app_config.toml_guide.md) and template [`docs/examples/app_config.template.toml`](docs/examples/app_config.template.toml).
289
+
290
+ ```toml
291
+ # Minimal recommended app_config.toml
292
+ knowledge_base_path = "E:/Knowledge_project/NoteConnection_app/Knowledge_Base"
293
+ user_language = "en"
294
+
295
+ [multi_window]
296
+ single_window_mode = true
297
+ hide_tauri_when_pathmode_opens = true
298
+ restore_tauri_when_pathmode_exits = true
299
+ confirm_before_full_shutdown_from_godot = true
300
+ sync_language = true
301
+ ```
285
302
 
286
303
  ## 🏗️ Build & Deployment
287
304
 
@@ -1136,9 +1153,26 @@ npm start -- --path "E:/Knowledge/ObsidianVault" --no-gpu
1136
1153
  管理知识库源现在变得更加简单。
1137
1154
 
1138
1155
  - **首次运行设置**: 首次启动时,系统会提示您选择 `Knowledge_Base` 文件夹。
1139
- - **持久化配置**: 您的选择保存在 `kb_config.json` 中,并在重启后自动加载。
1156
+ - **持久化配置 (`app_config.toml`)**: KB 路径、语言及多窗口偏好默认保存到 `%LOCALAPPDATA%/NoteConnection/app_config.toml`(Windows),重启后自动恢复。
1157
+ - **旧配置自动迁移**: 若同目录存在旧版 `kb_config.json`,启动时会自动迁移到 `app_config.toml`。
1140
1158
  - **随时更改**: 使用 **文件 > 更改知识库...** 菜单选项即时切换文件夹。
1141
1159
  - **重置**: 使用 **文件 > 重置为默认** 返回由捆绑的演示笔记。
1160
+ - **配置路径覆盖**: 可通过 `NOTE_CONNECTION_CONFIG_PATH`(完整文件路径)或 `NOTE_CONNECTION_CONFIG_DIR`(目录)自定义 `app_config.toml` 位置。
1161
+ - **窗口行为可调**: 在 `app_config.toml` 的 `[multi_window]` 段调整 `single_window_mode`、`hide_tauri_when_pathmode_opens`、`restore_tauri_when_pathmode_exits`、`confirm_before_full_shutdown_from_godot`、`sync_language`。
1162
+ - **详细配置说明**: 参见 [`docs/zh/app_config.toml_guide.md`](docs/zh/app_config.toml_guide.md) 与模板 [`docs/examples/app_config.template.toml`](docs/examples/app_config.template.toml)。
1163
+
1164
+ ```toml
1165
+ # 推荐最小 app_config.toml
1166
+ knowledge_base_path = "E:/Knowledge_project/NoteConnection_app/Knowledge_Base"
1167
+ user_language = "en"
1168
+
1169
+ [multi_window]
1170
+ single_window_mode = true
1171
+ hide_tauri_when_pathmode_opens = true
1172
+ restore_tauri_when_pathmode_exits = true
1173
+ confirm_before_full_shutdown_from_godot = true
1174
+ sync_language = true
1175
+ ```
1142
1176
 
1143
1177
  ## 🏗️ 构建与部署 (Build & Deployment)
1144
1178
 
@@ -180,6 +180,7 @@ const ALLOWED_CONFIG_STRATEGY_VALUES = new Set(['foundational', 'core']);
180
180
  const ALLOWED_CONFIG_LAYOUT_VALUES = new Set(['vertical', 'horizontal', 'radial', 'orbital']);
181
181
  const ALLOWED_READING_MODE_VALUES = new Set(['window', 'fullscreen']);
182
182
  const ALLOWED_READER_RENDER_MODE_VALUES = new Set(['render', 'source']);
183
+ const ALLOWED_CONFIG_LANGUAGE_VALUES = new Set(['en', 'zh']);
183
184
  const ALLOWED_BACKGROUND_FILE_EXTENSIONS = ['.exr', '.hdr'];
184
185
  const CONFIG_TARGET_ID_MAX_LENGTH = 512;
185
186
  const CONFIG_SHORTCUT_MAX_LENGTH = 64;
@@ -195,6 +196,7 @@ const ALLOWED_CONFIG_KEYS = new Set([
195
196
  'mode',
196
197
  'strategy',
197
198
  'layout',
199
+ 'language',
198
200
  'targetId',
199
201
  'target_id',
200
202
  'targetIds',
@@ -345,6 +347,15 @@ function validateConfigurePayload(payload, policy) {
345
347
  return `configure payload.layout must be one of: ${Array.from(ALLOWED_CONFIG_LAYOUT_VALUES).join(', ')}.`;
346
348
  }
347
349
  }
350
+ if (payload.language !== undefined) {
351
+ if (!isNonEmptyString(payload.language)) {
352
+ return 'configure payload.language must be a non-empty string when provided.';
353
+ }
354
+ const normalizedLanguage = payload.language.trim().toLowerCase();
355
+ if (!ALLOWED_CONFIG_LANGUAGE_VALUES.has(normalizedLanguage)) {
356
+ return `configure payload.language must be one of: ${Array.from(ALLOWED_CONFIG_LANGUAGE_VALUES).join(', ')}.`;
357
+ }
358
+ }
348
359
  if (payload.targetId !== undefined && typeof payload.targetId !== 'string') {
349
360
  return 'configure payload.targetId must be a string when provided.';
350
361
  }
@@ -279,9 +279,26 @@ npm start -- --path "E:/Knowledge/ObsidianVault" --no-gpu
279
279
  Managing your knowledge base source is now easier than ever.
280
280
 
281
281
  - **First Run Setup**: On first launch, you will be prompted to select your `Knowledge_Base` folder.
282
- - **Persistent Config**: Your selection is saved in `kb_config.json` and remembered across restarts.
282
+ - **Persistent Config (`app_config.toml`)**: Your KB path, language, and multi-window preferences are saved in `%LOCALAPPDATA%/NoteConnection/app_config.toml` (Windows default) and remembered across restarts.
283
+ - **Legacy Auto-Migration**: If a legacy `kb_config.json` exists in the same config directory, NoteConnection automatically migrates it to `app_config.toml`.
283
284
  - **Change Anytime**: Use the **File > Change Knowledge Base...** menu option to switch folders instantly.
284
285
  - **Reset**: Use **File > Reset to Default** to return to the bundled demo notes.
286
+ - **Config Path Overrides**: Set `NOTE_CONNECTION_CONFIG_PATH` (full file path) or `NOTE_CONNECTION_CONFIG_DIR` (directory) to customize where `app_config.toml` is stored.
287
+ - **Window Behavior Tuning**: Edit `[multi_window]` in `app_config.toml` (`single_window_mode`, `hide_tauri_when_pathmode_opens`, `restore_tauri_when_pathmode_exits`, `confirm_before_full_shutdown_from_godot`, `sync_language`).
288
+ - **Detailed Config Guide**: See [`docs/en/app_config.toml_guide.md`](docs/en/app_config.toml_guide.md) and template [`docs/examples/app_config.template.toml`](docs/examples/app_config.template.toml).
289
+
290
+ ```toml
291
+ # Minimal recommended app_config.toml
292
+ knowledge_base_path = "E:/Knowledge_project/NoteConnection_app/Knowledge_Base"
293
+ user_language = "en"
294
+
295
+ [multi_window]
296
+ single_window_mode = true
297
+ hide_tauri_when_pathmode_opens = true
298
+ restore_tauri_when_pathmode_exits = true
299
+ confirm_before_full_shutdown_from_godot = true
300
+ sync_language = true
301
+ ```
285
302
 
286
303
  ## 🏗️ Build & Deployment
287
304
 
@@ -1136,9 +1153,26 @@ npm start -- --path "E:/Knowledge/ObsidianVault" --no-gpu
1136
1153
  管理知识库源现在变得更加简单。
1137
1154
 
1138
1155
  - **首次运行设置**: 首次启动时,系统会提示您选择 `Knowledge_Base` 文件夹。
1139
- - **持久化配置**: 您的选择保存在 `kb_config.json` 中,并在重启后自动加载。
1156
+ - **持久化配置 (`app_config.toml`)**: KB 路径、语言及多窗口偏好默认保存到 `%LOCALAPPDATA%/NoteConnection/app_config.toml`(Windows),重启后自动恢复。
1157
+ - **旧配置自动迁移**: 若同目录存在旧版 `kb_config.json`,启动时会自动迁移到 `app_config.toml`。
1140
1158
  - **随时更改**: 使用 **文件 > 更改知识库...** 菜单选项即时切换文件夹。
1141
1159
  - **重置**: 使用 **文件 > 重置为默认** 返回由捆绑的演示笔记。
1160
+ - **配置路径覆盖**: 可通过 `NOTE_CONNECTION_CONFIG_PATH`(完整文件路径)或 `NOTE_CONNECTION_CONFIG_DIR`(目录)自定义 `app_config.toml` 位置。
1161
+ - **窗口行为可调**: 在 `app_config.toml` 的 `[multi_window]` 段调整 `single_window_mode`、`hide_tauri_when_pathmode_opens`、`restore_tauri_when_pathmode_exits`、`confirm_before_full_shutdown_from_godot`、`sync_language`。
1162
+ - **详细配置说明**: 参见 [`docs/zh/app_config.toml_guide.md`](docs/zh/app_config.toml_guide.md) 与模板 [`docs/examples/app_config.template.toml`](docs/examples/app_config.template.toml)。
1163
+
1164
+ ```toml
1165
+ # 推荐最小 app_config.toml
1166
+ knowledge_base_path = "E:/Knowledge_project/NoteConnection_app/Knowledge_Base"
1167
+ user_language = "en"
1168
+
1169
+ [multi_window]
1170
+ single_window_mode = true
1171
+ hide_tauri_when_pathmode_opens = true
1172
+ restore_tauri_when_pathmode_exits = true
1173
+ confirm_before_full_shutdown_from_godot = true
1174
+ sync_language = true
1175
+ ```
1142
1176
 
1143
1177
  ## 🏗️ 构建与部署 (Build & Deployment)
1144
1178
 
@@ -458,6 +458,40 @@ function isGraphA11yZhMode() {
458
458
  return String(window.i18n.currentLanguage).toLowerCase().startsWith('zh');
459
459
  }
460
460
 
461
+ function getRuntimeAppConfig() {
462
+ if (
463
+ window.NoteConnectionRuntime &&
464
+ typeof window.NoteConnectionRuntime.getAppRuntimeConfig === 'function'
465
+ ) {
466
+ return window.NoteConnectionRuntime.getAppRuntimeConfig();
467
+ }
468
+ if (window.__NC_APP_CONFIG && typeof window.__NC_APP_CONFIG === 'object') {
469
+ return window.__NC_APP_CONFIG;
470
+ }
471
+ return null;
472
+ }
473
+
474
+ function resolveRuntimeMultiWindowOptions() {
475
+ const defaults = {
476
+ singleWindowMode: true,
477
+ hideTauriWhenPathmodeOpens: true,
478
+ restoreTauriWhenPathmodeExits: true,
479
+ confirmBeforeFullShutdownFromGodot: true,
480
+ syncLanguage: true
481
+ };
482
+ const config = getRuntimeAppConfig();
483
+ if (!config || typeof config !== 'object' || !config.multiWindow || typeof config.multiWindow !== 'object') {
484
+ return defaults;
485
+ }
486
+ const next = { ...defaults };
487
+ Object.keys(defaults).forEach((key) => {
488
+ if (typeof config.multiWindow[key] === 'boolean') {
489
+ next[key] = config.multiWindow[key];
490
+ }
491
+ });
492
+ return next;
493
+ }
494
+
461
495
  function getGraphRendererMode() {
462
496
  const checked = document.querySelector('input[name="rendererMode"]:checked');
463
497
  if (!checked || (checked.value !== 'svg' && checked.value !== 'canvas')) {
@@ -967,10 +1001,13 @@ document.querySelectorAll('input[name="degreeMode"]').forEach(radio => {
967
1001
  // Localization is now handled by i18n.js
968
1002
  // We just need to listen for changes to update dynamic UI components like Analysis Panel
969
1003
  if (window.i18n) {
970
- window.i18n.onLanguageChange(() => {
1004
+ window.i18n.onLanguageChange((newLang) => {
971
1005
  if (typeof window.updateAnalysisUI === 'function') {
972
1006
  window.updateAnalysisUI();
973
1007
  }
1008
+ if (window.pathApp && typeof window.pathApp.syncLanguageWithBridge === 'function') {
1009
+ window.pathApp.syncLanguageWithBridge(newLang);
1010
+ }
974
1011
  scheduleGraphSemanticA11yRefresh('Language changed');
975
1012
  });
976
1013
  }
@@ -3525,6 +3562,22 @@ if (
3525
3562
  window.__TAURI__.event.listen('notemd-open-request', () => {
3526
3563
  showEmbeddedNoteMD({ source: 'tauri-event' });
3527
3564
  });
3565
+ window.__TAURI__.event.listen('app-language-updated', async (event) => {
3566
+ const language = event && event.payload && typeof event.payload.language === 'string'
3567
+ ? event.payload.language
3568
+ : '';
3569
+ if (!language || !window.i18n || typeof window.i18n.setLanguage !== 'function') {
3570
+ return;
3571
+ }
3572
+ if (window.i18n.currentLanguage === language) {
3573
+ return;
3574
+ }
3575
+ try {
3576
+ await window.i18n.setLanguage(language);
3577
+ } catch (error) {
3578
+ console.warn('[i18n] Failed to apply app-language-updated event payload:', error);
3579
+ }
3580
+ });
3528
3581
  }
3529
3582
 
3530
3583
  if (btnNotemd) {
@@ -3571,8 +3624,10 @@ if (btnPathMode) {
3571
3624
  // We check `nodes` length directly because it is the effective runtime source here.
3572
3625
  const hasData = (typeof nodes !== 'undefined' && nodes.length > 0);
3573
3626
 
3574
- if (!hasData) {
3575
- const msg = (window.i18n && window.i18n.currentLanguage === 'zh') ? "请先加载知识库。" : "Please load a Knowledge Base first.";
3627
+ if (!hasData) {
3628
+ const msg = (window.i18n && typeof window.i18n.t === 'function')
3629
+ ? window.i18n.t('pathMode.loadKbFirst')
3630
+ : 'Please load a Knowledge Base first.';
3576
3631
 
3577
3632
  // Inline Feedback
3578
3633
  let feedbackEl = document.getElementById('path-mode-feedback');
@@ -3600,6 +3655,7 @@ if (btnPathMode) {
3600
3655
  // Check for active selection for Diffusion Learning
3601
3656
  const highlightState = window.highlightManager ? window.highlightManager.getState() : null;
3602
3657
  const selectedNode = (highlightState && highlightState.currentNode) ? highlightState.currentNode : null;
3658
+ const multiWindowOptions = resolveRuntimeMultiWindowOptions();
3603
3659
 
3604
3660
  console.log('[Path Mode] Entering...', selectedNode ? `Target: ${selectedNode.id}` : 'Domain Mode');
3605
3661
 
@@ -3634,14 +3690,19 @@ if (btnPathMode) {
3634
3690
  if (window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function') {
3635
3691
  try {
3636
3692
  // 1. Hide the Tauri window via Rust IPC.
3637
- await window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: true });
3638
- // 2. Send setWindowVisible to Godot via PathBridge WebSocket.
3639
- if (window.pathApp && window.pathApp.ws && window.pathApp.ws.readyState === WebSocket.OPEN) {
3640
- window.pathApp.ws.send(JSON.stringify({
3641
- type: 'setWindowVisible',
3642
- payload: { visible: true }
3643
- }));
3644
- }
3693
+ await window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: true });
3694
+ // 2. Send setWindowVisible to Godot via PathBridge WebSocket.
3695
+ if (
3696
+ multiWindowOptions.singleWindowMode &&
3697
+ window.pathApp &&
3698
+ window.pathApp.ws &&
3699
+ window.pathApp.ws.readyState === WebSocket.OPEN
3700
+ ) {
3701
+ window.pathApp.ws.send(JSON.stringify({
3702
+ type: 'setWindowVisible',
3703
+ payload: { visible: true }
3704
+ }));
3705
+ }
3645
3706
  console.log('[Path Mode] Single-window toggle: Tauri hidden, Godot shown.');
3646
3707
  } catch (err) {
3647
3708
  console.warn('[Path Mode] toggle_pathmode_window failed:', err);
@@ -211,12 +211,15 @@
211
211
  "controls": "Controls"
212
212
  }
213
213
  },
214
- "notifications": {
215
- "buildSuccess": "Build Success! Reloading interface...",
216
- "buildFailed": "Build failed. Please check your knowledge base path.",
217
- "noData": "No data available. Please load a knowledge base first."
218
- },
219
- "manual": {
214
+ "notifications": {
215
+ "buildSuccess": "Build Success! Reloading interface...",
216
+ "buildFailed": "Build failed. Please check your knowledge base path.",
217
+ "noData": "No data available. Please load a knowledge base first."
218
+ },
219
+ "pathMode": {
220
+ "loadKbFirst": "Please load a Knowledge Base first."
221
+ },
222
+ "manual": {
220
223
  "loading": "Loading documentation...",
221
224
  "error": "Error Loading Documentation",
222
225
  "errorDetail": "Could not load User_Manual.md or README.md.",
@@ -211,12 +211,15 @@
211
211
  "controls": "控制"
212
212
  }
213
213
  },
214
- "notifications": {
215
- "buildSuccess": "构建成功!重新加载界面...",
216
- "buildFailed": "构建失败。请检查您的知识库路径。",
217
- "noData": "无可用数据。请先加载知识库。"
218
- },
219
- "nodes": "节点",
214
+ "notifications": {
215
+ "buildSuccess": "构建成功!重新加载界面...",
216
+ "buildFailed": "构建失败。请检查您的知识库路径。",
217
+ "noData": "无可用数据。请先加载知识库。"
218
+ },
219
+ "pathMode": {
220
+ "loadKbFirst": "请先加载知识库。"
221
+ },
222
+ "nodes": "节点",
220
223
  "edges": "边",
221
224
  "show_all": "显示全部",
222
225
  "show_in": "仅入度",
@@ -34,7 +34,8 @@ window.pathApp = {
34
34
  autoReconstruct: true,
35
35
  retainHistory: true
36
36
  },
37
- bridgeMermaidRenderQueue: Promise.resolve(),
37
+ bridgeLanguageListenerRegistered: false,
38
+ bridgeMermaidRenderQueue: Promise.resolve(),
38
39
  semanticA11yLastSummaryKey: '',
39
40
  semanticA11yLastAnnouncementAt: 0,
40
41
 
@@ -95,10 +96,12 @@ window.pathApp = {
95
96
  this.ws = socket;
96
97
  }
97
98
 
98
- this.ws.onopen = () => {
99
- console.log('[PathApp] Connected to Bridge');
100
- this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
101
- };
99
+ this.ws.onopen = () => {
100
+ console.log('[PathApp] Connected to Bridge');
101
+ this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
102
+ this._ensureLanguageSyncListener();
103
+ this.syncLanguageWithBridge();
104
+ };
102
105
  this.ws.onmessage = (e) => {
103
106
  try {
104
107
  const msg = this._parseBridgeIncomingMessage(e.data);
@@ -246,20 +249,24 @@ window.pathApp = {
246
249
  console.warn('[PathApp] Bridge socket error:', err);
247
250
  };
248
251
 
249
- if (hasActiveSocket && this.ws.readyState === WebSocket.OPEN) {
250
- console.log('[PathApp] Reusing existing Bridge socket');
251
- this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
252
- }
253
- },
254
-
255
- setupWebSocket: function() {
256
- if (!this._supportsSidecarBridge()) {
257
- console.log('[PathApp] Sidecar bridge is disabled for this runtime; skipping setupWebSocket.');
258
- return;
259
- }
252
+ if (hasActiveSocket && this.ws.readyState === WebSocket.OPEN) {
253
+ console.log('[PathApp] Reusing existing Bridge socket');
254
+ this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
255
+ this._ensureLanguageSyncListener();
256
+ this.syncLanguageWithBridge();
257
+ }
258
+ },
260
259
 
261
- const bridge = (typeof window !== 'undefined') ? window.NoteConnectionRuntime : null;
262
- const waitForRuntime = this._isTauriMode() && bridge && typeof bridge.whenReady === 'function';
260
+ setupWebSocket: function() {
261
+ if (!this._supportsSidecarBridge()) {
262
+ console.log('[PathApp] Sidecar bridge is disabled for this runtime; skipping setupWebSocket.');
263
+ return;
264
+ }
265
+
266
+ this._ensureLanguageSyncListener();
267
+
268
+ const bridge = (typeof window !== 'undefined') ? window.NoteConnectionRuntime : null;
269
+ const waitForRuntime = this._isTauriMode() && bridge && typeof bridge.whenReady === 'function';
263
270
 
264
271
  if (!waitForRuntime) {
265
272
  this._connectBridgeSocket();
@@ -344,14 +351,110 @@ window.pathApp = {
344
351
  return '';
345
352
  },
346
353
 
347
- _getBridgeIdentifyPayload: function(clientTag) {
348
- const payload = { client: clientTag };
349
- const authToken = this._getBridgeAuthToken();
350
- if (authToken) {
351
- payload.token = authToken;
352
- }
353
- return payload;
354
- },
354
+ _getBridgeIdentifyPayload: function(clientTag) {
355
+ const payload = { client: clientTag };
356
+ const authToken = this._getBridgeAuthToken();
357
+ if (authToken) {
358
+ payload.token = authToken;
359
+ }
360
+ return payload;
361
+ },
362
+
363
+ _normalizeLanguageCode: function(rawLanguage) {
364
+ const value = String(rawLanguage || '').trim().toLowerCase();
365
+ return value.startsWith('zh') ? 'zh' : 'en';
366
+ },
367
+
368
+ _getActiveLanguage: function() {
369
+ if (window.i18n && typeof window.i18n.currentLanguage === 'string') {
370
+ return this._normalizeLanguageCode(window.i18n.currentLanguage);
371
+ }
372
+ const languageSelect = document.getElementById('set-language');
373
+ if (languageSelect && typeof languageSelect.value === 'string') {
374
+ return this._normalizeLanguageCode(languageSelect.value);
375
+ }
376
+ const appConfig = this._getAppRuntimeConfig();
377
+ if (appConfig && typeof appConfig.language === 'string') {
378
+ return this._normalizeLanguageCode(appConfig.language);
379
+ }
380
+ return 'en';
381
+ },
382
+
383
+ _getAppRuntimeConfig: function() {
384
+ if (
385
+ typeof window !== 'undefined' &&
386
+ window.NoteConnectionRuntime &&
387
+ typeof window.NoteConnectionRuntime.getAppRuntimeConfig === 'function'
388
+ ) {
389
+ return window.NoteConnectionRuntime.getAppRuntimeConfig();
390
+ }
391
+ if (typeof window !== 'undefined' && window.__NC_APP_CONFIG && typeof window.__NC_APP_CONFIG === 'object') {
392
+ return window.__NC_APP_CONFIG;
393
+ }
394
+ return null;
395
+ },
396
+
397
+ _resolveMultiWindowOptions: function() {
398
+ const defaults = {
399
+ singleWindowMode: true,
400
+ hideTauriWhenPathmodeOpens: true,
401
+ restoreTauriWhenPathmodeExits: true,
402
+ confirmBeforeFullShutdownFromGodot: true,
403
+ syncLanguage: true
404
+ };
405
+ const appConfig = this._getAppRuntimeConfig();
406
+ if (!appConfig || !appConfig.multiWindow || typeof appConfig.multiWindow !== 'object') {
407
+ return defaults;
408
+ }
409
+ return {
410
+ singleWindowMode: typeof appConfig.multiWindow.singleWindowMode === 'boolean'
411
+ ? appConfig.multiWindow.singleWindowMode
412
+ : defaults.singleWindowMode,
413
+ hideTauriWhenPathmodeOpens: typeof appConfig.multiWindow.hideTauriWhenPathmodeOpens === 'boolean'
414
+ ? appConfig.multiWindow.hideTauriWhenPathmodeOpens
415
+ : defaults.hideTauriWhenPathmodeOpens,
416
+ restoreTauriWhenPathmodeExits: typeof appConfig.multiWindow.restoreTauriWhenPathmodeExits === 'boolean'
417
+ ? appConfig.multiWindow.restoreTauriWhenPathmodeExits
418
+ : defaults.restoreTauriWhenPathmodeExits,
419
+ confirmBeforeFullShutdownFromGodot: typeof appConfig.multiWindow.confirmBeforeFullShutdownFromGodot === 'boolean'
420
+ ? appConfig.multiWindow.confirmBeforeFullShutdownFromGodot
421
+ : defaults.confirmBeforeFullShutdownFromGodot,
422
+ syncLanguage: typeof appConfig.multiWindow.syncLanguage === 'boolean'
423
+ ? appConfig.multiWindow.syncLanguage
424
+ : defaults.syncLanguage
425
+ };
426
+ },
427
+
428
+ _buildLanguageConfigurePayload: function(rawLanguage) {
429
+ return {
430
+ language: this._normalizeLanguageCode(rawLanguage || this._getActiveLanguage())
431
+ };
432
+ },
433
+
434
+ syncLanguageWithBridge: function(rawLanguage) {
435
+ const options = this._resolveMultiWindowOptions();
436
+ if (!options.syncLanguage) {
437
+ return false;
438
+ }
439
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
440
+ return false;
441
+ }
442
+ const payload = this._buildLanguageConfigurePayload(rawLanguage);
443
+ return this._sendBridgeMessage('configure', payload);
444
+ },
445
+
446
+ _ensureLanguageSyncListener: function() {
447
+ if (this.bridgeLanguageListenerRegistered) {
448
+ return;
449
+ }
450
+ if (!window.i18n || typeof window.i18n.onLanguageChange !== 'function') {
451
+ return;
452
+ }
453
+ this.bridgeLanguageListenerRegistered = true;
454
+ window.i18n.onLanguageChange((lang) => {
455
+ this.syncLanguageWithBridge(lang);
456
+ });
457
+ },
355
458
 
356
459
  _getRuntimeBridgeAdapter: function() {
357
460
  if (
@@ -1543,40 +1646,60 @@ window.pathApp = {
1543
1646
 
1544
1647
  if (pathContainer) pathContainer.style.display = 'none';
1545
1648
  if (graphWrapper) graphWrapper.style.display = 'block';
1546
- if (sidebar) {
1547
- sidebar.style.transform = 'translateX(100%)';
1548
- sidebar.style.display = 'none';
1549
- }
1649
+ if (sidebar) {
1650
+ sidebar.style.transform = 'translateX(100%)';
1651
+ sidebar.style.display = 'none';
1652
+ }
1653
+ const multiWindowOptions = this._resolveMultiWindowOptions();
1550
1654
 
1551
1655
  // Single-window toggle: hide Godot, show Tauri.
1552
1656
  // 单窗口切换:隐藏 Godot,显示 Tauri。
1553
1657
  if (window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function') {
1554
1658
  // 1. Hide Godot window via PathBridge WebSocket.
1555
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1659
+ if (
1660
+ multiWindowOptions.singleWindowMode &&
1661
+ this.ws &&
1662
+ this.ws.readyState === WebSocket.OPEN
1663
+ ) {
1556
1664
  this.ws.send(JSON.stringify({
1557
1665
  type: 'setWindowVisible',
1558
1666
  payload: { visible: false }
1559
1667
  }));
1560
1668
  }
1561
1669
  // 2. Restore Tauri window via Rust IPC.
1562
- window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: false })
1563
- .then(() => console.log('[PathApp] Single-window toggle: Godot hidden, Tauri restored.'))
1564
- .catch((err) => console.warn('[PathApp] toggle_pathmode_window restore failed:', err));
1670
+ if (multiWindowOptions.restoreTauriWhenPathmodeExits) {
1671
+ window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: false })
1672
+ .then(() => console.log('[PathApp] Single-window toggle: Godot hidden, Tauri restored.'))
1673
+ .catch((err) => console.warn('[PathApp] toggle_pathmode_window restore failed:', err));
1674
+ }
1565
1675
  }
1566
1676
 
1567
- window.dispatchEvent(new Event('resize'));
1568
- },
1677
+ window.dispatchEvent(new Event('resize'));
1678
+ },
1569
1679
 
1570
1680
  applyRemoteConfigure: function(config) {
1571
1681
  if (!config || typeof config !== 'object') return;
1572
1682
 
1573
- const incomingTargetId = typeof config.targetId === 'string'
1574
- ? config.targetId
1575
- : (typeof config.target_id === 'string' ? config.target_id : null);
1576
-
1577
- if (typeof config.mode === 'string') {
1578
- this.runtimeConfig.mode = config.mode === 'diffusion' ? 'diffusion' : 'domain';
1579
- }
1683
+ const incomingTargetId = typeof config.targetId === 'string'
1684
+ ? config.targetId
1685
+ : (typeof config.target_id === 'string' ? config.target_id : null);
1686
+
1687
+ if (typeof config.language === 'string') {
1688
+ const normalizedLanguage = this._normalizeLanguageCode(config.language);
1689
+ if (
1690
+ window.i18n &&
1691
+ typeof window.i18n.setLanguage === 'function' &&
1692
+ window.i18n.currentLanguage !== normalizedLanguage
1693
+ ) {
1694
+ void window.i18n.setLanguage(normalizedLanguage).catch((error) => {
1695
+ console.warn('[PathApp] Failed to apply language from remote configure payload:', error);
1696
+ });
1697
+ }
1698
+ }
1699
+
1700
+ if (typeof config.mode === 'string') {
1701
+ this.runtimeConfig.mode = config.mode === 'diffusion' ? 'diffusion' : 'domain';
1702
+ }
1580
1703
  if (typeof config.strategy === 'string') {
1581
1704
  this.runtimeConfig.strategy = config.strategy === 'core' ? 'core' : 'foundational';
1582
1705
  }
@@ -3056,11 +3179,13 @@ window.pathApp = {
3056
3179
  return;
3057
3180
  }
3058
3181
 
3059
- this.ws.onopen = () => {
3060
- console.log('[PathApp] Early WS Connected to Bridge');
3061
- this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend-early'));
3062
-
3063
- const initialCentralId = this._getPreferredStandaloneCentralId(preferredCentralId);
3182
+ this.ws.onopen = () => {
3183
+ console.log('[PathApp] Early WS Connected to Bridge');
3184
+ this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend-early'));
3185
+ this._ensureLanguageSyncListener();
3186
+ this.syncLanguageWithBridge();
3187
+
3188
+ const initialCentralId = this._getPreferredStandaloneCentralId(preferredCentralId);
3064
3189
  if (initialCentralId) {
3065
3190
  this.centralNodeId = initialCentralId;
3066
3191
  this.sendPathToBridgeStandalone(initialCentralId);
@@ -98,6 +98,31 @@
98
98
  return value.replace(/\/+$/, '');
99
99
  }
100
100
 
101
+ function normalizeLanguageCode(rawValue) {
102
+ const value = String(rawValue || '').trim().toLowerCase();
103
+ return value.startsWith('zh') ? 'zh' : 'en';
104
+ }
105
+
106
+ function normalizeBoolean(value, fallback) {
107
+ if (typeof value === 'boolean') {
108
+ return value;
109
+ }
110
+ return fallback;
111
+ }
112
+
113
+ function getDefaultAppRuntimeConfig() {
114
+ return {
115
+ language: 'en',
116
+ multiWindow: {
117
+ singleWindowMode: true,
118
+ hideTauriWhenPathmodeOpens: true,
119
+ restoreTauriWhenPathmodeExits: true,
120
+ confirmBeforeFullShutdownFromGodot: true,
121
+ syncLanguage: true
122
+ }
123
+ };
124
+ }
125
+
101
126
  const state = {
102
127
  baseUrl: normalizeBaseUrl(window.__NC_SIDECAR_RUNTIME && window.__NC_SIDECAR_RUNTIME.baseUrl),
103
128
  bridgeWsUrl: normalizeBridgeWsUrl(window.__NC_SIDECAR_RUNTIME && window.__NC_SIDECAR_RUNTIME.bridgeWsUrl),
@@ -106,6 +131,34 @@
106
131
  port: Number((window.__NC_SIDECAR_RUNTIME && window.__NC_SIDECAR_RUNTIME.port) || 3000),
107
132
  bridgePort: Number((window.__NC_SIDECAR_RUNTIME && window.__NC_SIDECAR_RUNTIME.bridgePort) || 9876)
108
133
  };
134
+ const appState = getDefaultAppRuntimeConfig();
135
+ if (window.__NC_APP_CONFIG && typeof window.__NC_APP_CONFIG === 'object') {
136
+ const bootAppConfig = window.__NC_APP_CONFIG;
137
+ appState.language = normalizeLanguageCode(bootAppConfig.language || appState.language);
138
+ if (bootAppConfig.multiWindow && typeof bootAppConfig.multiWindow === 'object') {
139
+ const bootMultiWindow = bootAppConfig.multiWindow;
140
+ appState.multiWindow.singleWindowMode = normalizeBoolean(
141
+ bootMultiWindow.singleWindowMode,
142
+ appState.multiWindow.singleWindowMode
143
+ );
144
+ appState.multiWindow.hideTauriWhenPathmodeOpens = normalizeBoolean(
145
+ bootMultiWindow.hideTauriWhenPathmodeOpens,
146
+ appState.multiWindow.hideTauriWhenPathmodeOpens
147
+ );
148
+ appState.multiWindow.restoreTauriWhenPathmodeExits = normalizeBoolean(
149
+ bootMultiWindow.restoreTauriWhenPathmodeExits,
150
+ appState.multiWindow.restoreTauriWhenPathmodeExits
151
+ );
152
+ appState.multiWindow.confirmBeforeFullShutdownFromGodot = normalizeBoolean(
153
+ bootMultiWindow.confirmBeforeFullShutdownFromGodot,
154
+ appState.multiWindow.confirmBeforeFullShutdownFromGodot
155
+ );
156
+ appState.multiWindow.syncLanguage = normalizeBoolean(
157
+ bootMultiWindow.syncLanguage,
158
+ appState.multiWindow.syncLanguage
159
+ );
160
+ }
161
+ }
109
162
 
110
163
  let runtimeReadyResolved = false;
111
164
  let runtimeHydrationPromise = null;
@@ -126,6 +179,20 @@
126
179
  return window.__NC_SIDECAR_RUNTIME;
127
180
  }
128
181
 
182
+ function syncGlobalAppState() {
183
+ window.__NC_APP_CONFIG = {
184
+ language: appState.language,
185
+ multiWindow: {
186
+ singleWindowMode: appState.multiWindow.singleWindowMode,
187
+ hideTauriWhenPathmodeOpens: appState.multiWindow.hideTauriWhenPathmodeOpens,
188
+ restoreTauriWhenPathmodeExits: appState.multiWindow.restoreTauriWhenPathmodeExits,
189
+ confirmBeforeFullShutdownFromGodot: appState.multiWindow.confirmBeforeFullShutdownFromGodot,
190
+ syncLanguage: appState.multiWindow.syncLanguage
191
+ }
192
+ };
193
+ return window.__NC_APP_CONFIG;
194
+ }
195
+
129
196
  function finalizeRuntimeReady() {
130
197
  if (runtimeReadyResolved) {
131
198
  return runtimeReadyPromise;
@@ -170,6 +237,44 @@
170
237
  return syncGlobalState();
171
238
  }
172
239
 
240
+ function setAppRuntimeConfig(nextConfig) {
241
+ const merged = getDefaultAppRuntimeConfig();
242
+ if (nextConfig && typeof nextConfig === 'object') {
243
+ merged.language = normalizeLanguageCode(nextConfig.language || merged.language);
244
+ const nextMultiWindow = nextConfig.multiWindow && typeof nextConfig.multiWindow === 'object'
245
+ ? nextConfig.multiWindow
246
+ : {};
247
+ merged.multiWindow.singleWindowMode = normalizeBoolean(
248
+ nextMultiWindow.singleWindowMode,
249
+ merged.multiWindow.singleWindowMode
250
+ );
251
+ merged.multiWindow.hideTauriWhenPathmodeOpens = normalizeBoolean(
252
+ nextMultiWindow.hideTauriWhenPathmodeOpens,
253
+ merged.multiWindow.hideTauriWhenPathmodeOpens
254
+ );
255
+ merged.multiWindow.restoreTauriWhenPathmodeExits = normalizeBoolean(
256
+ nextMultiWindow.restoreTauriWhenPathmodeExits,
257
+ merged.multiWindow.restoreTauriWhenPathmodeExits
258
+ );
259
+ merged.multiWindow.confirmBeforeFullShutdownFromGodot = normalizeBoolean(
260
+ nextMultiWindow.confirmBeforeFullShutdownFromGodot,
261
+ merged.multiWindow.confirmBeforeFullShutdownFromGodot
262
+ );
263
+ merged.multiWindow.syncLanguage = normalizeBoolean(
264
+ nextMultiWindow.syncLanguage,
265
+ merged.multiWindow.syncLanguage
266
+ );
267
+ }
268
+
269
+ appState.language = merged.language;
270
+ appState.multiWindow = merged.multiWindow;
271
+ return syncGlobalAppState();
272
+ }
273
+
274
+ function getAppRuntimeConfig() {
275
+ return syncGlobalAppState();
276
+ }
277
+
173
278
  function buildUrl(resourcePath, query) {
174
279
  const normalizedPath = String(resourcePath || '').replace(/^\/+/, '');
175
280
  const url = new URL(normalizedPath, `${state.baseUrl}/`);
@@ -292,6 +397,11 @@
292
397
  };
293
398
  }
294
399
 
400
+ } catch (error) {
401
+ console.warn('[RuntimeBridge] Failed to hydrate runtime capabilities from Tauri. Using runtime bridge defaults.', error);
402
+ }
403
+
404
+ try {
295
405
  const runtimeCaps = window.__NC_RUNTIME_CAPS || {};
296
406
  if (runtimeCaps.supports_sidecar) {
297
407
  const runtimeConfig = await invokeTauriWithTimeout(
@@ -305,11 +415,23 @@
305
415
  console.warn('[RuntimeBridge] Failed to hydrate sidecar runtime config from Tauri. Using runtime bridge defaults.', error);
306
416
  }
307
417
 
418
+ try {
419
+ const appConfig = await invokeTauriWithTimeout(
420
+ () => invoke('get_app_runtime_config'),
421
+ 'get_app_runtime_config',
422
+ TAURI_RUNTIME_HYDRATE_TIMEOUT_MS
423
+ );
424
+ setAppRuntimeConfig(appConfig);
425
+ } catch (error) {
426
+ console.warn('[RuntimeBridge] Failed to hydrate app runtime config from Tauri. Using runtime defaults.', error);
427
+ }
428
+
308
429
  if (typeof window.dispatchEvent === 'function' && typeof window.CustomEvent === 'function') {
309
430
  window.dispatchEvent(new CustomEvent('noteconnection:runtime-ready', {
310
431
  detail: {
311
432
  runtime: syncGlobalState(),
312
- caps: window.__NC_RUNTIME_CAPS || null
433
+ caps: window.__NC_RUNTIME_CAPS || null,
434
+ appConfig: syncGlobalAppState()
313
435
  }
314
436
  }));
315
437
  }
@@ -330,6 +452,8 @@
330
452
  window.NoteConnectionRuntime = {
331
453
  setRuntimeConfig,
332
454
  getRuntimeConfig,
455
+ setAppRuntimeConfig,
456
+ getAppRuntimeConfig,
333
457
  buildUrl,
334
458
  buildFetchOptions,
335
459
  createAuthHeaders,
@@ -349,6 +473,7 @@
349
473
  };
350
474
 
351
475
  syncGlobalState();
476
+ syncGlobalAppState();
352
477
 
353
478
  if (document.readyState === 'loading') {
354
479
  document.addEventListener('DOMContentLoaded', () => {
@@ -41,6 +41,15 @@ const child_process_1 = require("child_process");
41
41
  function readJson(filePath) {
42
42
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
43
43
  }
44
+ function buildUnsignedGeneratorEnv() {
45
+ return {
46
+ ...process.env,
47
+ NOTE_CONNECTION_SBOM_SIGNING_PRIVATE_KEY_PEM: '',
48
+ NOTE_CONNECTION_SBOM_SIGNING_PRIVATE_KEY_FILE: '',
49
+ NOTE_CONNECTION_SBOM_ATTESTATION_ENABLE_TRANSPARENCY_LOG: '0',
50
+ NOTE_CONNECTION_SBOM_ATTESTATION_TRANSPARENCY_LOG_PATH: '',
51
+ };
52
+ }
44
53
  describe('sbom attestation policy contract', () => {
45
54
  const repoRoot = path.resolve(__dirname, '..');
46
55
  const packageJsonPath = path.join(repoRoot, 'package.json');
@@ -48,6 +57,33 @@ describe('sbom attestation policy contract', () => {
48
57
  const verifierPath = path.join(repoRoot, 'scripts', 'verify-sbom-attestation.js');
49
58
  const migrationWorkflowPath = path.join(repoRoot, '.github', 'workflows', 'migration-gates.yml');
50
59
  const npmPublishWorkflowPath = path.join(repoRoot, '.github', 'workflows', 'npm-publish.yml');
60
+ let noteConnectionEnvSnapshot;
61
+ beforeEach(() => {
62
+ // Keep contract tests hermetic even when CI injects NOTE_CONNECTION_* policy env vars.
63
+ noteConnectionEnvSnapshot = {};
64
+ for (const key of Object.keys(process.env)) {
65
+ if (!key.startsWith('NOTE_CONNECTION_')) {
66
+ continue;
67
+ }
68
+ noteConnectionEnvSnapshot[key] = process.env[key];
69
+ delete process.env[key];
70
+ }
71
+ });
72
+ afterEach(() => {
73
+ for (const key of Object.keys(process.env)) {
74
+ if (key.startsWith('NOTE_CONNECTION_')) {
75
+ delete process.env[key];
76
+ }
77
+ }
78
+ for (const [key, value] of Object.entries(noteConnectionEnvSnapshot || {})) {
79
+ if (typeof value === 'undefined') {
80
+ delete process.env[key];
81
+ }
82
+ else {
83
+ process.env[key] = value;
84
+ }
85
+ }
86
+ });
51
87
  test('exports attestation generation/verification scripts and gate wiring', () => {
52
88
  const packageJson = readJson(packageJsonPath);
53
89
  const scripts = packageJson.scripts || {};
@@ -124,6 +160,7 @@ describe('sbom attestation policy contract', () => {
124
160
  expect(npmPublishWorkflow).toContain('NOTE_CONNECTION_SBOM_ATTESTATION_VERIFY_TRANSPARENCY_LOG_INCLUSION');
125
161
  expect(npmPublishWorkflow).toContain('NOTE_CONNECTION_SBOM_ATTESTATION_TRANSPARENCY_EXPECT_SCHEMA');
126
162
  expect(npmPublishWorkflow).toContain('NOTE_CONNECTION_SBOM_ATTESTATION_TRANSPARENCY_EXPECT_VERSION');
163
+ expect(npmPublishWorkflow).toContain('npx jest --runInBand');
127
164
  });
128
165
  test('attestation generator and verifier work in unsigned strict policy mode', () => {
129
166
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'noteconnection-sbom-attestation-'));
@@ -142,6 +179,7 @@ describe('sbom attestation policy contract', () => {
142
179
  cwd: repoRoot,
143
180
  encoding: 'utf8',
144
181
  stdio: 'pipe',
182
+ env: buildUnsignedGeneratorEnv(),
145
183
  });
146
184
  expect(generateResult.status).toBe(0);
147
185
  const verifyResult = (0, child_process_1.spawnSync)(process.execPath, [
@@ -183,6 +221,7 @@ describe('sbom attestation policy contract', () => {
183
221
  cwd: repoRoot,
184
222
  encoding: 'utf8',
185
223
  stdio: 'pipe',
224
+ env: buildUnsignedGeneratorEnv(),
186
225
  });
187
226
  expect(generateResult.status).toBe(0);
188
227
  const verifyResult = (0, child_process_1.spawnSync)(process.execPath, [
@@ -232,6 +271,7 @@ describe('sbom attestation policy contract', () => {
232
271
  cwd: repoRoot,
233
272
  encoding: 'utf8',
234
273
  stdio: 'pipe',
274
+ env: buildUnsignedGeneratorEnv(),
235
275
  });
236
276
  expect(generateResult.status).toBe(0);
237
277
  const verifyResult = (0, child_process_1.spawnSync)(process.execPath, [
@@ -275,6 +315,7 @@ describe('sbom attestation policy contract', () => {
275
315
  cwd: repoRoot,
276
316
  encoding: 'utf8',
277
317
  stdio: 'pipe',
318
+ env: buildUnsignedGeneratorEnv(),
278
319
  });
279
320
  expect(generateResult.status).toBe(0);
280
321
  const verifyResult = (0, child_process_1.spawnSync)(process.execPath, [
@@ -243,6 +243,7 @@ describe('server migration settings routes', () => {
243
243
  let buildGraphMock;
244
244
  let renderMathPngMock;
245
245
  let renderMermaidPngMock;
246
+ let copyPngToClipboardMock;
246
247
  let originalArgv;
247
248
  beforeAll(async () => {
248
249
  temp = new TempDir('noteconnection-server');
@@ -283,6 +284,7 @@ describe('server migration settings routes', () => {
283
284
  width: 640,
284
285
  height: 360
285
286
  });
287
+ copyPngToClipboardMock = jest.fn().mockResolvedValue(undefined);
286
288
  jest.resetModules();
287
289
  originalArgv = [...process.argv];
288
290
  process.argv = process.argv.slice(0, 2);
@@ -296,6 +298,9 @@ describe('server migration settings routes', () => {
296
298
  renderMathPng: renderMathPngMock,
297
299
  renderMermaidPng: renderMermaidPngMock
298
300
  }));
301
+ jest.doMock('./native_clipboard', () => ({
302
+ copyPngToClipboard: copyPngToClipboardMock
303
+ }));
299
304
  const serverModule = require('./server');
300
305
  server = await serverModule.startServer({ port });
301
306
  buildGraphMock.mockClear();
@@ -319,6 +324,7 @@ describe('server migration settings routes', () => {
319
324
  jest.dontMock('./index');
320
325
  jest.dontMock('./core/PathBridge');
321
326
  jest.dontMock('./reader_renderer');
327
+ jest.dontMock('./native_clipboard');
322
328
  process.argv = originalArgv;
323
329
  temp.cleanup();
324
330
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noteconnection",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Hierarchical Knowledge Graph Visualization System",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {
@@ -74,8 +74,8 @@
74
74
  "test": "jest",
75
75
  "cleanup:tauri:sidecars": "node scripts/cleanup-tauri-sidecars.js",
76
76
  "docs:diataxis:check": "node scripts/verify-diataxis-map.js",
77
- "docs:site:build": "mkdocs build --config-file mkdocs.yml",
78
- "docs:site:serve": "mkdocs serve --config-file mkdocs.yml"
77
+ "docs:site:build": "node scripts/run-mkdocs.js build --config-file mkdocs.yml",
78
+ "docs:site:serve": "node scripts/run-mkdocs.js serve --config-file mkdocs.yml"
79
79
  },
80
80
  "pkg": {
81
81
  "scripts": [