dsh-vscode-mode 0.1.35 → 0.1.42

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/lib/client.js CHANGED
@@ -385,6 +385,523 @@ window.__ModuleLoader__.load({
385
385
  window.dispatchEvent(new CustomEvent("edrv:show-launcher", { detail: { tab } }));
386
386
  }
387
387
  //#endregion
388
+ //#region src/client/monaco/theme.ts
389
+ /**
390
+ * dsh-vscode-mode client — Monaco 语法分色主题(rich token 配色)。
391
+ * 编辑器原先硬编码 'vs'(内置基础主题),token 分色层次少;
392
+ * 这里定义 edrv-dark / edrv-light 两套完整 token 配色(对齐 VSCode Dark+ 语义分层),
393
+ * 并随 DSH 明暗主题自动切换(跟随文档根 data 主题属性 / prefers-color-scheme)。
394
+ * 作者 ddj 2026-08-28
395
+ */
396
+ const EDRV_DARK = "edrv-dark";
397
+ const EDRV_LIGHT = "edrv-light";
398
+ /** 语法分色(语义分层:关键字/字符串/数字/注释/函数/变量/类型/运算符/正则/标签/属性…)。 */
399
+ const DARK_RULES = [
400
+ {
401
+ token: "",
402
+ foreground: "d4d4d4",
403
+ background: "1e1e1e"
404
+ },
405
+ {
406
+ token: "comment",
407
+ foreground: "6a9955",
408
+ fontStyle: "italic"
409
+ },
410
+ {
411
+ token: "comment.doc",
412
+ foreground: "7ca97c",
413
+ fontStyle: "italic"
414
+ },
415
+ {
416
+ token: "keyword",
417
+ foreground: "c586c0"
418
+ },
419
+ {
420
+ token: "keyword.control",
421
+ foreground: "c586c0"
422
+ },
423
+ {
424
+ token: "keyword.operator",
425
+ foreground: "d4d4d4"
426
+ },
427
+ {
428
+ token: "keyword.json",
429
+ foreground: "9cdcfe"
430
+ },
431
+ {
432
+ token: "string",
433
+ foreground: "ce9178"
434
+ },
435
+ {
436
+ token: "string.escape",
437
+ foreground: "d7ba7d"
438
+ },
439
+ {
440
+ token: "string.key.json",
441
+ foreground: "9cdcfe"
442
+ },
443
+ {
444
+ token: "string.value.json",
445
+ foreground: "ce9178"
446
+ },
447
+ {
448
+ token: "string.yaml",
449
+ foreground: "ce9178"
450
+ },
451
+ {
452
+ token: "number",
453
+ foreground: "b5cea8"
454
+ },
455
+ {
456
+ token: "number.hex",
457
+ foreground: "b5cea8"
458
+ },
459
+ {
460
+ token: "regexp",
461
+ foreground: "d16969"
462
+ },
463
+ {
464
+ token: "type",
465
+ foreground: "4ec9b0"
466
+ },
467
+ {
468
+ token: "type.identifier",
469
+ foreground: "4ec9b0"
470
+ },
471
+ {
472
+ token: "identifier",
473
+ foreground: "d4d4d4"
474
+ },
475
+ {
476
+ token: "variable",
477
+ foreground: "9cdcfe"
478
+ },
479
+ {
480
+ token: "variable.global",
481
+ foreground: "ff6699"
482
+ },
483
+ {
484
+ token: "variable.local",
485
+ foreground: "9cdcfe"
486
+ },
487
+ {
488
+ token: "variable.mutable",
489
+ foreground: "9cdcfe"
490
+ },
491
+ {
492
+ token: "variable.predefined",
493
+ foreground: "4fc1ff"
494
+ },
495
+ {
496
+ token: "variable.parameter",
497
+ foreground: "00bfae"
498
+ },
499
+ {
500
+ token: "parameter",
501
+ foreground: "00bfae"
502
+ },
503
+ {
504
+ token: "function",
505
+ foreground: "dcdcaa"
506
+ },
507
+ {
508
+ token: "method",
509
+ foreground: "dcdcaa"
510
+ },
511
+ {
512
+ token: "property",
513
+ foreground: "9cdcfe"
514
+ },
515
+ {
516
+ token: "function.identifier",
517
+ foreground: "dcdcaa"
518
+ },
519
+ {
520
+ token: "member",
521
+ foreground: "dcdcaa"
522
+ },
523
+ {
524
+ token: "constant",
525
+ foreground: "4fc1ff"
526
+ },
527
+ {
528
+ token: "constant.numeric",
529
+ foreground: "b5cea8"
530
+ },
531
+ {
532
+ token: "constant.language",
533
+ foreground: "569cd6"
534
+ },
535
+ {
536
+ token: "tag",
537
+ foreground: "569cd6"
538
+ },
539
+ {
540
+ token: "tag.xml",
541
+ foreground: "569cd6"
542
+ },
543
+ {
544
+ token: "metatag",
545
+ foreground: "808080"
546
+ },
547
+ {
548
+ token: "attribute.name",
549
+ foreground: "9cdcfe"
550
+ },
551
+ {
552
+ token: "attribute.value",
553
+ foreground: "ce9178"
554
+ },
555
+ {
556
+ token: "delimiter",
557
+ foreground: "d4d4d4"
558
+ },
559
+ {
560
+ token: "delimiter.bracket",
561
+ foreground: "ffd700"
562
+ },
563
+ {
564
+ token: "delimiter.html",
565
+ foreground: "808080"
566
+ },
567
+ {
568
+ token: "operator",
569
+ foreground: "d4d4d4"
570
+ },
571
+ {
572
+ token: "namespace",
573
+ foreground: "4ec9b0"
574
+ },
575
+ {
576
+ token: "class",
577
+ foreground: "4ec9b0"
578
+ },
579
+ {
580
+ token: "interface",
581
+ foreground: "4ec9b0"
582
+ },
583
+ {
584
+ token: "enum",
585
+ foreground: "4ec9b0"
586
+ },
587
+ {
588
+ token: "struct",
589
+ foreground: "4ec9b0"
590
+ },
591
+ {
592
+ token: "annotation",
593
+ foreground: "dcdcaa"
594
+ },
595
+ {
596
+ token: "punctuation",
597
+ foreground: "d4d4d4"
598
+ },
599
+ {
600
+ token: "invalid",
601
+ foreground: "f44747"
602
+ }
603
+ ];
604
+ const LIGHT_RULES = [
605
+ {
606
+ token: "",
607
+ foreground: "1e1e1e",
608
+ background: "ffffff"
609
+ },
610
+ {
611
+ token: "comment",
612
+ foreground: "008000",
613
+ fontStyle: "italic"
614
+ },
615
+ {
616
+ token: "comment.doc",
617
+ foreground: "2e7d32",
618
+ fontStyle: "italic"
619
+ },
620
+ {
621
+ token: "keyword",
622
+ foreground: "0000ff"
623
+ },
624
+ {
625
+ token: "keyword.control",
626
+ foreground: "af00db"
627
+ },
628
+ {
629
+ token: "keyword.operator",
630
+ foreground: "1e1e1e"
631
+ },
632
+ {
633
+ token: "keyword.json",
634
+ foreground: "0451a5"
635
+ },
636
+ {
637
+ token: "string",
638
+ foreground: "a31515"
639
+ },
640
+ {
641
+ token: "string.escape",
642
+ foreground: "ee0000"
643
+ },
644
+ {
645
+ token: "string.key.json",
646
+ foreground: "0451a5"
647
+ },
648
+ {
649
+ token: "string.value.json",
650
+ foreground: "a31515"
651
+ },
652
+ {
653
+ token: "string.yaml",
654
+ foreground: "a31515"
655
+ },
656
+ {
657
+ token: "number",
658
+ foreground: "098658"
659
+ },
660
+ {
661
+ token: "number.hex",
662
+ foreground: "098658"
663
+ },
664
+ {
665
+ token: "regexp",
666
+ foreground: "811f3f"
667
+ },
668
+ {
669
+ token: "type",
670
+ foreground: "267f99"
671
+ },
672
+ {
673
+ token: "type.identifier",
674
+ foreground: "267f99"
675
+ },
676
+ {
677
+ token: "identifier",
678
+ foreground: "1e1e1e"
679
+ },
680
+ {
681
+ token: "variable",
682
+ foreground: "001080"
683
+ },
684
+ {
685
+ token: "variable.global",
686
+ foreground: "c2185b"
687
+ },
688
+ {
689
+ token: "variable.local",
690
+ foreground: "001080"
691
+ },
692
+ {
693
+ token: "variable.mutable",
694
+ foreground: "001080"
695
+ },
696
+ {
697
+ token: "variable.predefined",
698
+ foreground: "001080"
699
+ },
700
+ {
701
+ token: "variable.parameter",
702
+ foreground: "008577"
703
+ },
704
+ {
705
+ token: "parameter",
706
+ foreground: "008577"
707
+ },
708
+ {
709
+ token: "function",
710
+ foreground: "795e26"
711
+ },
712
+ {
713
+ token: "method",
714
+ foreground: "795e26"
715
+ },
716
+ {
717
+ token: "function.identifier",
718
+ foreground: "795e26"
719
+ },
720
+ {
721
+ token: "member",
722
+ foreground: "795e26"
723
+ },
724
+ {
725
+ token: "property",
726
+ foreground: "001080"
727
+ },
728
+ {
729
+ token: "constant",
730
+ foreground: "0070c1"
731
+ },
732
+ {
733
+ token: "constant.numeric",
734
+ foreground: "098658"
735
+ },
736
+ {
737
+ token: "constant.language",
738
+ foreground: "0000ff"
739
+ },
740
+ {
741
+ token: "tag",
742
+ foreground: "800000"
743
+ },
744
+ {
745
+ token: "tag.xml",
746
+ foreground: "800000"
747
+ },
748
+ {
749
+ token: "metatag",
750
+ foreground: "6b6b6b"
751
+ },
752
+ {
753
+ token: "attribute.name",
754
+ foreground: "e50000"
755
+ },
756
+ {
757
+ token: "attribute.value",
758
+ foreground: "0451a5"
759
+ },
760
+ {
761
+ token: "delimiter",
762
+ foreground: "1e1e1e"
763
+ },
764
+ {
765
+ token: "delimiter.bracket",
766
+ foreground: "b46400"
767
+ },
768
+ {
769
+ token: "delimiter.html",
770
+ foreground: "800000"
771
+ },
772
+ {
773
+ token: "operator",
774
+ foreground: "1e1e1e"
775
+ },
776
+ {
777
+ token: "namespace",
778
+ foreground: "267f99"
779
+ },
780
+ {
781
+ token: "class",
782
+ foreground: "267f99"
783
+ },
784
+ {
785
+ token: "interface",
786
+ foreground: "267f99"
787
+ },
788
+ {
789
+ token: "enum",
790
+ foreground: "267f99"
791
+ },
792
+ {
793
+ token: "struct",
794
+ foreground: "267f99"
795
+ },
796
+ {
797
+ token: "annotation",
798
+ foreground: "795e26"
799
+ },
800
+ {
801
+ token: "punctuation",
802
+ foreground: "1e1e1e"
803
+ },
804
+ {
805
+ token: "invalid",
806
+ foreground: "cd3131"
807
+ }
808
+ ];
809
+ /** 编辑器 UI 配色(与语法配色同套,保证 gutter/minimap/selection 一致)。 */
810
+ const DARK_COLORS = {
811
+ "editor.background": "#1e1e1e",
812
+ "editor.foreground": "#d4d4d4",
813
+ "editorLineNumber.foreground": "#6e7681",
814
+ "editorLineNumber.activeForeground": "#c6c6c6",
815
+ "editor.selectionBackground": "#264f78",
816
+ "editor.inactiveSelectionBackground": "#3a3d41",
817
+ "editor.lineHighlightBackground": "#2a2d2e",
818
+ "editorCursor.foreground": "#aeafad",
819
+ "editorIndentGuide.background1": "#404040",
820
+ "editorIndentGuide.activeBackground1": "#707070",
821
+ "editorWidget.background": "#252526",
822
+ "editorHoverWidget.background": "#252526",
823
+ "editorGutter.background": "#1e1e1e",
824
+ "minimap.background": "#1e1e1e",
825
+ "scrollbarSlider.background": "#79797966",
826
+ "scrollbarSlider.hoverBackground": "#646464b3",
827
+ "scrollbarSlider.activeBackground": "#bfbfbf66"
828
+ };
829
+ const LIGHT_COLORS = {
830
+ "editor.background": "#ffffff",
831
+ "editor.foreground": "#1e1e1e",
832
+ "editorLineNumber.foreground": "#6c6c6c",
833
+ "editorLineNumber.activeForeground": "#171184",
834
+ "editor.selectionBackground": "#add6ff",
835
+ "editor.inactiveSelectionBackground": "#e5ebf1",
836
+ "editor.lineHighlightBackground": "#f0f0f0",
837
+ "editorCursor.foreground": "#000000",
838
+ "editorIndentGuide.background1": "#d3d3d3",
839
+ "editorIndentGuide.activeBackground1": "#939393",
840
+ "editorWidget.background": "#f3f3f3",
841
+ "editorHoverWidget.background": "#f3f3f3",
842
+ "editorGutter.background": "#ffffff",
843
+ "minimap.background": "#ffffff",
844
+ "scrollbarSlider.background": "#64646466",
845
+ "scrollbarSlider.hoverBackground": "#646464b3",
846
+ "scrollbarSlider.activeBackground": "#00000099"
847
+ };
848
+ /**
849
+ * 注册 edrv 双套主题(幂等;Monaco 未加载时静默跳过)。
850
+ * @author ddj 2026年08月28号
851
+ * @param monaco Monaco 实例(window.monaco)
852
+ */
853
+ function registerThemes(monaco) {
854
+ if (!monaco?.editor?.defineTheme) return;
855
+ try {
856
+ monaco.editor.defineTheme(EDRV_DARK, {
857
+ base: "vs-dark",
858
+ inherit: true,
859
+ rules: DARK_RULES,
860
+ colors: DARK_COLORS
861
+ });
862
+ monaco.editor.defineTheme(EDRV_LIGHT, {
863
+ base: "vs",
864
+ inherit: true,
865
+ rules: LIGHT_RULES,
866
+ colors: LIGHT_COLORS
867
+ });
868
+ } catch (error) {}
869
+ }
870
+ /**
871
+ * 探测当前 DSH 明暗(文档根 data 主题属性 → prefers-color-scheme → 暗)。
872
+ * @author ddj 2026年08月28号
873
+ * @returns 'dark' | 'light'
874
+ */
875
+ function detectColorScheme() {
876
+ try {
877
+ const attr = document.documentElement?.getAttribute?.("data-theme") ?? document.body?.getAttribute?.("data-theme");
878
+ const value = String(attr ?? "").toLowerCase();
879
+ if (value.includes("light") || value.includes("catppuccin-latte")) return "light";
880
+ if (value.includes("dark") || value.includes("frappe") || value.includes("macchiato") || value.includes("mocha")) return "dark";
881
+ if (typeof window.matchMedia === "function") return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
882
+ } catch (error) {}
883
+ return "dark";
884
+ }
885
+ /**
886
+ * 主题名:按当前明暗返回已注册主题(未注册回落 vs)。
887
+ * @author ddj 2026年08月28号
888
+ * @returns 主题 id
889
+ */
890
+ function themeNameOf() {
891
+ return detectColorScheme() === "light" ? EDRV_LIGHT : EDRV_DARK;
892
+ }
893
+ /**
894
+ * 应用主题到 Monaco(全局切换;新建编辑器也用该主题)。
895
+ * @author ddj 2026年08月28号
896
+ * @param monaco Monaco 实例
897
+ */
898
+ function applyTheme(monaco) {
899
+ if (!monaco?.editor?.setTheme) return;
900
+ try {
901
+ monaco.editor.setTheme(themeNameOf());
902
+ } catch (error) {}
903
+ }
904
+ //#endregion
388
905
  //#region src/client/monaco/loader.ts
389
906
  /**
390
907
  * dsh-vscode-mode client — Monaco 加载与语言映射(AMD 构建,离线随包分发)。
@@ -500,7 +1017,20 @@ window.__ModuleLoader__.load({
500
1017
  kt: "kotlin",
501
1018
  kts: "kotlin",
502
1019
  dart: "dart",
503
- dockerfile: "dockerfile"
1020
+ dockerfile: "dockerfile",
1021
+ csproj: "xml",
1022
+ props: "xml",
1023
+ targets: "xml",
1024
+ plist: "xml",
1025
+ lua51: "lua",
1026
+ luac: "lua",
1027
+ gitattributes: "ini",
1028
+ editorconfig: "ini",
1029
+ env: "ini",
1030
+ properties: "ini",
1031
+ json5: "jsonc",
1032
+ log: "plaintext",
1033
+ txt: "plaintext"
504
1034
  };
505
1035
  /**
506
1036
  * 路径 → Monaco language id:取 basename 扩展名(dockerfile/Dockerfile 特判)。
@@ -536,6 +1066,10 @@ window.__ModuleLoader__.load({
536
1066
  publishStage("core", MONACO_STAGES.core.progress, MONACO_STAGES.core.message);
537
1067
  window.require(["vs/editor/editor.main"], () => {
538
1068
  publishStage("ready", MONACO_STAGES.ready.progress, MONACO_STAGES.ready.message);
1069
+ try {
1070
+ registerThemes(window.monaco);
1071
+ applyTheme(window.monaco);
1072
+ } catch (error) {}
539
1073
  resolve(window.monaco);
540
1074
  }, (err) => fail(/* @__PURE__ */ new Error("Monaco 模块加载失败:" + String(err))));
541
1075
  } catch (error) {
@@ -1861,7 +2395,9 @@ window.__ModuleLoader__.load({
1861
2395
  /** 搜索面板条件(SearchPanel v1)。 */
1862
2396
  search: "edrv.search.v1.",
1863
2397
  /** 侧边栏提示已关闭标记(全局,无会话)。 */
1864
- sideHint: "edrv.side-hint-dismissed"
2398
+ sideHint: "edrv.side-hint-dismissed",
2399
+ /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
2400
+ workspaceFold: "edrv.ws-fold.v1."
1865
2401
  };
1866
2402
  //#endregion
1867
2403
  //#region src/client/state/viewStateCache.ts
@@ -1969,6 +2505,48 @@ window.__ModuleLoader__.load({
1969
2505
  }
1970
2506
  //#endregion
1971
2507
  //#region src/shared/lsp.ts
2508
+ /** LSP semantic token 标准类型(host 归一化后,client 与 Monaco 共用)。 */
2509
+ const LSP_SEMANTIC_TOKEN_TYPES = [
2510
+ "namespace",
2511
+ "type",
2512
+ "class",
2513
+ "enum",
2514
+ "interface",
2515
+ "struct",
2516
+ "typeParameter",
2517
+ "parameter",
2518
+ "variable",
2519
+ "property",
2520
+ "enumMember",
2521
+ "event",
2522
+ "function",
2523
+ "method",
2524
+ "macro",
2525
+ "label",
2526
+ "comment",
2527
+ "string",
2528
+ "keyword",
2529
+ "number",
2530
+ "regexp",
2531
+ "operator",
2532
+ "decorator"
2533
+ ];
2534
+ /** LSP semantic token 标准修饰符(host 归一化后,client 与 Monaco 共用)。 */
2535
+ const LSP_SEMANTIC_TOKEN_MODIFIERS = [
2536
+ "declaration",
2537
+ "definition",
2538
+ "readonly",
2539
+ "static",
2540
+ "deprecated",
2541
+ "abstract",
2542
+ "async",
2543
+ "modification",
2544
+ "documentation",
2545
+ "defaultLibrary",
2546
+ "global",
2547
+ "local",
2548
+ "mutable"
2549
+ ];
1972
2550
  /**
1973
2551
  * Monaco 位置(1-based lineNumber/column)→ LSP Position(0-based)。
1974
2552
  * @author ddj 2026年08月27号
@@ -2111,6 +2689,16 @@ window.__ModuleLoader__.load({
2111
2689
  if (!res || !res.ok) return null;
2112
2690
  return res.hover || null;
2113
2691
  }
2692
+ /** 查询当前文档的 semantic tokens(host 已归一化 legend 与 delta data)。 */
2693
+ async function fetchSemanticTokens(path, text) {
2694
+ await syncDoc(path, text, true);
2695
+ const res = await rpc("edrv.lsp.semanticTokens", {
2696
+ sessionId,
2697
+ path
2698
+ }).catch(() => null);
2699
+ if (!res || !res.ok || !res.tokens || !Array.isArray(res.tokens.data)) return null;
2700
+ return res.tokens;
2701
+ }
2114
2702
  /** 拉取服务器状态(带 5s 缓存)。 */
2115
2703
  async function refreshStatus(force = false) {
2116
2704
  if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers;
@@ -2130,7 +2718,10 @@ window.__ModuleLoader__.load({
2130
2718
  const end = range.end || {};
2131
2719
  return {
2132
2720
  uri: window.monaco.Uri.parse(toEdrvUri(loc.uri, loc.root)),
2133
- range: new window.monaco.Range(Math.max(1, (start.line ?? 0) + 1), Math.max(1, (start.character ?? 0) + 1), Math.max(1, (end.line ?? 0) + 1), Math.max(1, (end.character ?? 0) + 1))
2721
+ range: new window.monaco.Range(Math.max(1, (start.line ?? 0) + 1), Math.max(1, (start.character ?? 0) + 1), Math.max(1, (end.line ?? 0) + 1), Math.max(1, (end.character ?? 0) + 1)),
2722
+ lspUri: loc.uri,
2723
+ lspRoot: loc.root,
2724
+ lspRange: loc.range
2134
2725
  };
2135
2726
  }
2136
2727
  /** file:// URI(或裸路径)→ edrv:// 相对路径(去掉盘符/根,与编辑器模型路径一致)。 */
@@ -2142,30 +2733,68 @@ window.__ModuleLoader__.load({
2142
2733
  }
2143
2734
  /** 将 LSP 目标解析为当前工作区相对路径(跨文件跳转与 model URI 对齐)。 */
2144
2735
  function targetOpenPath(loc) {
2145
- return relativeLspPath(loc?.uri, loc?.root);
2736
+ return relativeLspPath(loc?.lspUri ?? loc?.uri, loc?.lspRoot ?? loc?.root);
2146
2737
  }
2147
- /** file URI/裸路径 → root 下的 / 分隔相对路径;root 不匹配时保留规范化绝对路径。 */
2148
- function relativeLspPath(uri, root) {
2738
+ /**
2739
+ * 归一化 Location 的 uri → 工作区路径(剥离 scheme,不补盘符)。
2740
+ * 三种输入形态(缺一即导致 scheme 泄漏进文件路径 → 跳转"文件不存在"):
2741
+ * - Monaco Uri 对象(toClientLocation 产物,scheme=edrv)→ 取 .path(已是工作区相对路径)
2742
+ * - 'edrv:///x' 字符串 → 取路径部分
2743
+ * - 'file:///D:/x' → 去 authority 空段保留盘符
2744
+ * @author ddj 2026年08月28号
2745
+ * @param uri Monaco Uri 对象 / file:// 或 edrv:// URI / 裸路径
2746
+ * @returns / 分隔的路径(edrv 相对路径 或 带盘符绝对路径)
2747
+ */
2748
+ function lspUriToAbs(uri) {
2149
2749
  if (!uri) return "";
2150
- let path = String(uri);
2151
- if (path.startsWith("file://")) try {
2152
- path = decodeURIComponent(path.replace(/^file:\/\//, ""));
2750
+ const isObject = typeof uri === "object";
2751
+ const raw = isObject && typeof uri.path === "string" ? uri.path : String(uri);
2752
+ let path = raw;
2753
+ if (path.startsWith("file://")) {
2754
+ try {
2755
+ path = decodeURIComponent(path.replace(/^file:\/\/\/?/, ""));
2756
+ } catch (error) {
2757
+ path = path.replace(/^file:\/\/\/?/, "");
2758
+ }
2759
+ return path.replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1");
2760
+ }
2761
+ if (path.startsWith("edrv://")) try {
2762
+ path = decodeURIComponent(path.replace(/^edrv:\/\/\/?/, ""));
2153
2763
  } catch (error) {
2154
- path = path.replace(/^file:\/\//, "");
2764
+ path = path.replace(/^edrv:\/\/\/?/, "");
2155
2765
  }
2156
- path = path.replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1");
2766
+ if (isObject ? uri.scheme === "edrv" : raw.startsWith("edrv://")) return path.replace(/\\/g, "/").replace(/^\/+/, "");
2767
+ return path.replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1");
2768
+ }
2769
+ /**
2770
+ * file URI/裸路径 → root 下的 / 分隔相对路径。
2771
+ * root 匹配返回工作区相对路径;root 不匹配返回完整绝对路径(保留盘符),
2772
+ * 供 edrv.read 按绝对路径解析(原先错误剥掉首个 / 导致跳转"文件不存在")。
2773
+ * @author ddj 2026年08月28号
2774
+ * @param uri file:// URI 或裸路径
2775
+ * @param root 工作区根(可选)
2776
+ * @returns 可 edrv.read 的路径
2777
+ */
2778
+ function relativeLspPath(uri, root) {
2779
+ if (!uri) return "";
2780
+ const path = lspUriToAbs(uri);
2157
2781
  const normalizedRoot = root ? String(root).replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1").replace(/\/+$/, "") : "";
2158
2782
  const lowerPath = path.toLowerCase();
2159
2783
  const lowerRoot = normalizedRoot.toLowerCase();
2160
2784
  if (normalizedRoot && (lowerPath === lowerRoot || lowerPath.startsWith(lowerRoot + "/"))) return path.slice(normalizedRoot.length).replace(/^\/+/, "");
2161
- return path.replace(/^\/+/, "");
2785
+ return path;
2162
2786
  }
2163
2787
  /** 目标行/列(0-based LSP → 1-based Monaco)。 */
2164
2788
  function targetMonoPosition(loc) {
2165
- const start = (loc.range || {}).start || {};
2789
+ const lspRange = loc?.lspRange;
2790
+ if (lspRange?.start) return {
2791
+ line: Math.max(1, (lspRange.start.line ?? 0) + 1),
2792
+ column: Math.max(1, (lspRange.start.character ?? 0) + 1)
2793
+ };
2794
+ const range = loc?.range ?? {};
2166
2795
  return {
2167
- line: Math.max(1, (start.line ?? 0) + 1),
2168
- column: Math.max(1, (start.character ?? 0) + 1)
2796
+ line: Math.max(1, range.startLineNumber ?? 1),
2797
+ column: Math.max(1, range.startColumn ?? 1)
2169
2798
  };
2170
2799
  }
2171
2800
  //#endregion
@@ -2178,6 +2807,10 @@ window.__ModuleLoader__.load({
2178
2807
  * 作者 ddj 2026-08-27
2179
2808
  */
2180
2809
  const LSP_LANGS = ["lua", "csharp"];
2810
+ const SEMANTIC_LEGEND = {
2811
+ tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
2812
+ tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
2813
+ };
2181
2814
  let registered = false;
2182
2815
  const disposables = [];
2183
2816
  let overlayEl = null;
@@ -2212,6 +2845,19 @@ window.__ModuleLoader__.load({
2212
2845
  };
2213
2846
  disposables.push(monaco.editor.onDidCreateModel(attachModel));
2214
2847
  for (const model of monaco.editor.getModels()) attachModel(model);
2848
+ const onRedetect = (event) => {
2849
+ const languageId = event?.detail?.languageId;
2850
+ if (!languageId) return;
2851
+ for (const model of monaco.editor.getModels()) {
2852
+ const path = pathOfModel(model);
2853
+ if (!path) continue;
2854
+ if (model.getLanguageId && model.getLanguageId() !== languageId) continue;
2855
+ syncDoc(path, model.getValue(), true);
2856
+ }
2857
+ refreshStatus(true);
2858
+ };
2859
+ window.addEventListener("edrv:lsp-redetect", onRedetect);
2860
+ disposables.push(() => window.removeEventListener("edrv:lsp-redetect", onRedetect));
2215
2861
  disposables.push(monaco.languages.registerDefinitionProvider(LSP_LANGS, { provideDefinition: (model, position, token) => {
2216
2862
  const path = pathOfModel(model);
2217
2863
  if (!path) return [];
@@ -2234,7 +2880,17 @@ window.__ModuleLoader__.load({
2234
2880
  contents: hover.contents.map((text) => ({ value: text })),
2235
2881
  range: range || void 0
2236
2882
  };
2237
- } }));
2883
+ } }), monaco.languages.registerDocumentSemanticTokensProvider(LSP_LANGS, {
2884
+ getLegend: () => SEMANTIC_LEGEND,
2885
+ provideDocumentSemanticTokens: async (model, token) => {
2886
+ const path = pathOfModel(model);
2887
+ if (!path || token?.isCancellationRequested) return { data: /* @__PURE__ */ new Uint32Array() };
2888
+ const result = await fetchSemanticTokens(path, model.getValue());
2889
+ if (!result || token?.isCancellationRequested) return { data: /* @__PURE__ */ new Uint32Array() };
2890
+ return { data: Uint32Array.from(result.data) };
2891
+ },
2892
+ releaseDocumentSemanticTokens: () => {}
2893
+ }));
2238
2894
  }
2239
2895
  /** LSP SymbolInfo(host 归一化后)→ Monaco DocumentSymbol。 */
2240
2896
  function toMonacoSymbol(symbol) {
@@ -2623,7 +3279,8 @@ window.__ModuleLoader__.load({
2623
3279
  }));
2624
3280
  setStatus("已加载");
2625
3281
  } else {
2626
- const message = res?.error ? String(res.error) : "读取失败";
3282
+ const base = res?.error ? String(res.error) : "读取失败";
3283
+ const message = res?.resolvedPath ? base + ":" + String(res.resolvedPath) : base;
2627
3284
  setLoadError(message);
2628
3285
  setError(message);
2629
3286
  setStatus("读取失败");
@@ -2890,6 +3547,20 @@ window.__ModuleLoader__.load({
2890
3547
  alive = false;
2891
3548
  };
2892
3549
  }, [monaco, monacoErr]);
3550
+ react.default.useEffect(() => {
3551
+ if (!monaco) return;
3552
+ registerThemes(monaco);
3553
+ applyTheme(monaco);
3554
+ if (typeof window.matchMedia !== "function") return;
3555
+ const query = window.matchMedia("(prefers-color-scheme: dark)");
3556
+ const onChange = () => applyTheme(monaco);
3557
+ if (typeof query.addEventListener === "function") {
3558
+ query.addEventListener("change", onChange);
3559
+ return () => query.removeEventListener("change", onChange);
3560
+ }
3561
+ query.addListener(onChange);
3562
+ return () => query.removeListener(onChange);
3563
+ }, [monaco]);
2893
3564
  monacoRef.current = monaco;
2894
3565
  const getModel = (path, text) => {
2895
3566
  const cache = modelsRef.current;
@@ -3003,7 +3674,7 @@ window.__ModuleLoader__.load({
3003
3674
  const ed = m.editor.create(node, {
3004
3675
  value: "",
3005
3676
  language: "plaintext",
3006
- theme: "vs",
3677
+ theme: themeNameOf(),
3007
3678
  fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)",
3008
3679
  fontSize: 13,
3009
3680
  lineHeight: 20,
@@ -4230,7 +4901,7 @@ window.__ModuleLoader__.load({
4230
4901
  }
4231
4902
  //#endregion
4232
4903
  //#region \0dsh-css:/home/runner/work/DSH_VsCodeMode/DSH_VsCodeMode/src/client/styles/mcp.css.mjs
4233
- const css = "/* dsh-vscode-mode MCP 管理界面样式。\n 全部使用 DSH 真实主题变量(--dsw-alias-*),不再用不存在的 fg-* 变量。 */\n.vsm-mcp-page{display:flex;flex-direction:column;gap:18px;width:100%;max-width:920px;box-sizing:border-box;color:var(--dsw-alias-label-primary,#e6e6e6);padding-bottom:32px}\n\n/* 顶部:标题 + 副标题 + 添加按钮 */\n.vsm-mcp-header{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}\n.vsm-mcp-header h2{margin:0 0 4px;font-size:20px;font-weight:600;line-height:1.35;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-mcp-header p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}\n.vsm-primary{border:0;border-radius:6px;padding:8px 16px;background:var(--dsw-alias-button-primary-fill,#6d55ff);color:var(--dsw-alias-button-contrast-fill,#fff);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}\n.vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}\n.vsm-primary:disabled{opacity:.55;cursor:default}\n\n/* 子 Tab:我的 MCP / MCP 市场 */\n.vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}\n.vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}\n.vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}\n.vsm-mcp-tabs button.active::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}\n\n/* 通用设置 */\n.vsm-general-page{display:flex;flex-direction:column;gap:10px;max-width:720px}\n.vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-general-page p{margin:0 0 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}\n.vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}\n.vsm-general-row select{min-width:220px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);font:inherit}\n.vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n\n/* 服务卡片 */\n.vsm-mcp-card{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2,transparent);transition:border-color .15s}\n.vsm-mcp-card:hover{border-color:var(--dsw-alias-border-l3,#555)}\n.vsm-mcp-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px}\n.vsm-mcp-title{display:flex;align-items:center;gap:9px;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6);min-width:0}\n.vsm-mcp-source{font-size:11px;font-weight:400;padding:2px 7px;border-radius:4px;background:var(--dsw-alias-bg-mask-2,#ffffff22);color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}\n.vsm-mcp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dsw-alias-state-error-primary,#e05252)}\n.vsm-mcp-dot.connected{background:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-mcp-dot.connecting{background:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-mcp-dot.disabled{background:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-mcp-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}\n.vsm-mcp-actions button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);cursor:pointer;font-size:15px;width:28px;height:28px;border-radius:6px;line-height:1}\n.vsm-mcp-actions button:hover{color:var(--dsw-alias-label-primary,#fff);background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-mcp-actions button.vsm-danger:hover{color:var(--dsw-alias-state-error-primary,#f07878);background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}\n.vsm-switch{font-size:14px !important}\n.vsm-switch.on{color:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-mcp-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:9px 0}\n.vsm-mcp-error{color:var(--dsw-alias-state-error-primary,#f07878);font-size:12px;margin:8px 0}\n.vsm-mcp-banner{padding:9px 12px;border:1px solid var(--dsw-alias-state-error-secondary,#d9534f55);border-radius:6px;background:var(--dsw-alias-state-error-secondary,#d9534f1a);margin-bottom:10px}\n\n/* 工具 chips */\n.vsm-mcp-tools{display:flex;flex-wrap:wrap;gap:6px}\n.vsm-mcp-chip{font:12px var(--ds-font-family-code,ui-monospace,monospace);padding:4px 8px;border-radius:5px;background:var(--dsw-alias-bg-mask-2,#ffffff14);color:var(--dsw-alias-label-secondary,#c8c8c8);border:1px solid var(--dsw-alias-border-l1,#333)}\n\n/* 空态 / 加载态 */\n.vsm-mcp-empty{text-align:center;padding:56px 20px;color:var(--dsw-alias-label-tertiary,#9aa5b1);border:1px dashed var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;font-size:13px}\n\n/* 项目搜索下拉框 */\n.vsm-project-picker{position:relative;width:100%;z-index:4}\n.vsm-project-picker-trigger{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,#e6e6e6);cursor:pointer;text-align:left}\n.vsm-project-picker-trigger:hover{border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-picker-label{display:flex;flex-direction:column;min-width:0;font-size:13px;font-weight:500}\n.vsm-project-picker-label small{margin-top:3px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-project-picker-arrow{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:16px}\n.vsm-project-picker-menu{position:absolute;top:calc(100% + 6px);left:0;right:0;padding:8px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:9px;background:var(--dsw-alias-bg-overlay,#202020);box-shadow:0 12px 36px #0008}\n.vsm-project-picker-search{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);outline:none}\n.vsm-project-picker-search:focus{border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-picker-options{max-height:260px;overflow:auto;margin-top:6px}\n.vsm-project-option{display:flex;width:100%;flex-direction:column;gap:3px;padding:8px 9px;border:0;border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;text-align:left}\n.vsm-project-option:hover,.vsm-project-option.highlight{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-project-option.selected{color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-option small{overflow:hidden;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);text-overflow:ellipsis;white-space:nowrap}\n.vsm-project-picker-empty{padding:18px 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:center}\n\n/* 项目级 MCP 分组 */\n.vsm-project{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;margin-bottom:14px;overflow:hidden;background:var(--dsw-alias-bg-layer-1,transparent)}\n.vsm-project-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:var(--dsw-alias-bg-mask-1,#ffffff0a)}\n.vsm-project-title{display:flex;align-items:center;gap:10px;min-width:0}\n.vsm-project-icon{color:var(--dsw-alias-brand-primary,#6d55ff);font-size:15px;flex-shrink:0}\n.vsm-project-name{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);display:block}\n.vsm-project-path{font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-family:var(--ds-font-family-code,ui-monospace,monospace);display:block;margin-top:2px;word-break:break-all}\n.vsm-primary.vsm-small{padding:5px 12px;font-size:12px}\n.vsm-project-empty{text-align:center;padding:26px 16px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px}\n.vsm-project .vsm-mcp-card{border:0;border-radius:0;margin:0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:transparent}\n.vsm-project .vsm-mcp-card:last-child{border-bottom:0}\n.vsm-project .vsm-mcp-banner{border-radius:0;margin:0}\n\n/* 新增 MCP 弹窗 */\n.vsm-mcp-modal{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,#000000aa);display:flex;align-items:center;justify-content:center;z-index:50}\n.vsm-mcp-dialog{width:min(560px,calc(100vw - 40px));max-height:88vh;overflow:auto;padding:22px 24px;border-radius:12px;background:var(--dsw-alias-bg-overlay,#202020);border:1px solid var(--dsw-alias-border-l2,#444);box-shadow:0 16px 60px #000c;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-mcp-dialog h3{margin:0 0 14px;font-size:16px;font-weight:600}\n.vsm-mcp-dialog label{display:flex;flex-direction:column;gap:6px;margin:12px 0;font-size:12px;color:var(--dsw-alias-label-secondary,#aaa)}\n.vsm-mcp-dialog input,.vsm-mcp-dialog select,.vsm-mcp-dialog textarea{font:inherit;color:var(--dsw-alias-label-primary,#eee);background:var(--dsw-alias-bg-layer-1,#2b2b2b);border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 10px}\n.vsm-mcp-dialog input:focus,.vsm-mcp-dialog select:focus,.vsm-mcp-dialog textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-mcp-dialog textarea{min-height:64px;resize:vertical;font-family:var(--ds-font-family-code,ui-monospace,monospace)}\n.vsm-mcp-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}\n.vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}\n.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n\n/* 兼容性小节 */\n.vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}\n.vsm-compat-item.ok .vsm-compat-name::before{content:\"● \";color:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-compat-item.warn .vsm-compat-name::before{content:\"● \";color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-compat-name{font-weight:500;min-width:0}\n.vsm-compat-note{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:right;min-width:0}\n\n/* 开发形态开关 */\n.vsm-devform-note{display:block;margin-top:4px;font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);word-break:break-all}\n.vsm-devform-actions{display:flex;flex-direction:column;align-items:flex-end;gap:6px;min-width:0}\n.vsm-devform-msg{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px;text-align:right}\n.vsm-general-row button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:6px 12px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}\n.vsm-general-row button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-general-row button:disabled{opacity:.55;cursor:default}\n.vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}\n\n/* keyboard shortcuts page */\n.vsm-kb-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}\n.vsm-kb-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#161616)}\n.vsm-kb-row.rec{border-color:var(--dsw-alias-brand-primary,#4f8cff)}\n.vsm-kb-label{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;color:var(--dsw-alias-label-primary,#eee);font-size:13px}\n.vsm-kb-warn{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px}\n.vsm-kb-key{min-width:96px;text-align:center;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-family:var(--ds-font-family-code,ui-monospace,monospace)}\n.vsm-kb-key.warn{border-color:var(--dsw-alias-state-warn-primary,#e5b93f);color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-kb-key.invalid{border-color:var(--dsw-alias-state-error-primary,#d9534f);color:var(--dsw-alias-state-error-primary,#d9534f)}\n.vsm-kb-rec{font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}\n.vsm-kb-actions{display:flex;gap:6px}\n.vsm-kb-actions button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:4px 10px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}\n.vsm-kb-actions button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-kb-actions button:disabled{opacity:.55;cursor:default}\n.vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}\n.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}\n\n/* 语言服务器(LSP)设置子 Tab */\n.vsm-lsp-form{display:flex;flex-direction:column;gap:6px;margin-top:8px}\n.vsm-lsp-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}\n.vsm-lsp-form input{height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;outline:none}\n.vsm-lsp-form input:focus{border-color:var(--dsw-alias-brand-primary,#4f8cff)}\n.vsm-lsp-form button{align-self:flex-start;margin-top:2px}\n.vsm-lsp-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin-top:10px;line-height:1.6}\n\n/* 会话性能(PerfSettings 子 Tab) */\n.vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}\n.vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}\n.vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}\n.vsm-perf-card{display:flex;flex-direction:column;gap:4px;min-width:120px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;background:var(--dsw-alias-bg-layer-2,transparent)}\n.vsm-perf-card span{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-card b{font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}\n.vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}\n.vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}\n.vsm-perf-ws-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}\n.vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}\n.vsm-perf-rows{display:flex;flex-direction:column}\n.vsm-perf-row{display:flex;align-items:center;gap:10px;padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-perf-row:last-child{border-bottom:0}\n.vsm-perf-row:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff0d)}\n.vsm-perf-row input[type=checkbox]{accent-color:var(--dsw-alias-brand-primary,#6d55ff);cursor:pointer}\n.vsm-perf-cell{min-width:0}\n.vsm-perf-id{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,monospace);color:var(--dsw-alias-label-primary,#eee)}\n.vsm-perf-size{width:70px;text-align:right;flex-shrink:0;color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}\n.vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}\n.vsm-perf-moved{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}\n.vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-config{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}\n.vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}\n.vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}\n.vsm-perf-guide{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}\n.vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}\n.vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-modal-card h3{margin:0;font-size:16px}\n.vsm-perf-modal-note{font-size:12px}\n.vsm-perf-modal-list{list-style:none;margin:0;padding:0;overflow-y:auto;font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8);display:flex;flex-direction:column;gap:4px}\n.vsm-perf-modal-list li{padding:4px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#262626);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-perf-modal-more{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}\n.vsm-secondary{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 16px;background:none;color:var(--dsw-alias-label-primary,#eee);font-size:13px;cursor:pointer;white-space:nowrap}\n.vsm-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-secondary:disabled{opacity:.55;cursor:default}\n.vsm-small{padding:4px 10px;font-size:12px}\n\n/* 扩展管理(LSP 语言服务器) */\n.vsm-mcp-actions button.vsm-primary,\n.vsm-mcp-actions button.vsm-danger{width:auto;height:auto;padding:5px 12px;font-size:12px;border-radius:6px;line-height:1.4}\n.vsm-mcp-actions button.vsm-danger{color:var(--dsw-alias-state-error-primary,#f07878);background:none}\n.vsm-mcp-actions button.vsm-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}\n.vsm-ext-ver{font-weight:400;font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n";
4904
+ const css = "/* dsh-vscode-mode MCP 管理界面样式。\n 全部使用 DSH 真实主题变量(--dsw-alias-*),不再用不存在的 fg-* 变量。 */\n.vsm-mcp-page{display:flex;flex-direction:column;gap:18px;width:100%;max-width:920px;box-sizing:border-box;color:var(--dsw-alias-label-primary,#e6e6e6);padding-bottom:32px}\n\n/* 顶部:标题 + 副标题 + 添加按钮 */\n.vsm-mcp-header{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}\n.vsm-mcp-header h2{margin:0 0 4px;font-size:20px;font-weight:600;line-height:1.35;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-mcp-header p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}\n.vsm-primary{border:0;border-radius:6px;padding:8px 16px;background:var(--dsw-alias-button-primary-fill,#6d55ff);color:var(--dsw-alias-button-contrast-fill,#fff);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}\n.vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}\n.vsm-primary:disabled{opacity:.55;cursor:default}\n\n/* MCP 管理下的二级子页签:我的 MCP / 项目 MCP / MCP 市场 */\n.vsm-mcp-subhead{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:-4px}\n.vsm-mcp-subtabs{display:flex;gap:18px;border-bottom:1px solid var(--dsw-alias-border-l1,#2e2e2e);flex:1}\n.vsm-mcp-subtabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:7px 2px 9px;cursor:pointer;font-size:13px;position:relative}\n.vsm-mcp-subtabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-mcp-subtabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}\n.vsm-mcp-subtabs button.active::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}\n\n/* 顶层 Tab:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性 */\n.vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}\n.vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}\n.vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}\n.vsm-mcp-tabs button.active::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}\n\n/* 统一线框面板:圆角矩形边框圈住各设置分区,提升可读性。\n 用法:<section class=\"vsm-panel\"> + 可选 <h3 class=\"vsm-panel-title\">。 */\n.vsm-panel{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,transparent);overflow:hidden}\n.vsm-panel-pad{padding:14px 16px}\n.vsm-panel-title{margin:0;padding:10px 16px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8);background:var(--dsw-alias-bg-mask-1,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l1,#333)}\n.vsm-panel-body{padding:6px 16px}\n.vsm-panel-body>*:first-child{margin-top:8px}\n.vsm-panel-body>*:last-child{margin-bottom:10px}\n\n/* 通用设置 */\n.vsm-general-page{display:flex;flex-direction:column;gap:14px;max-width:720px}\n.vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-general-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}\n.vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}\n.vsm-general-row:last-child{border-bottom:0}\n.vsm-general-row select{min-width:220px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);font:inherit}\n.vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n\n/* 服务卡片 */\n.vsm-mcp-card{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2,transparent);transition:border-color .15s}\n.vsm-mcp-card:hover{border-color:var(--dsw-alias-border-l3,#555)}\n.vsm-mcp-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px}\n.vsm-mcp-title{display:flex;align-items:center;gap:9px;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6);min-width:0}\n.vsm-mcp-source{font-size:11px;font-weight:400;padding:2px 7px;border-radius:4px;background:var(--dsw-alias-bg-mask-2,#ffffff22);color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}\n.vsm-mcp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dsw-alias-state-error-primary,#e05252)}\n.vsm-mcp-dot.connected{background:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-mcp-dot.connecting{background:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-mcp-dot.disabled{background:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-mcp-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}\n.vsm-mcp-actions button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);cursor:pointer;font-size:15px;width:28px;height:28px;border-radius:6px;line-height:1}\n.vsm-mcp-actions button:hover{color:var(--dsw-alias-label-primary,#fff);background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-mcp-actions button.vsm-danger:hover{color:var(--dsw-alias-state-error-primary,#f07878);background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}\n.vsm-switch{font-size:14px !important}\n.vsm-switch.on{color:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-mcp-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:9px 0}\n.vsm-mcp-error{color:var(--dsw-alias-state-error-primary,#f07878);font-size:12px;margin:8px 0}\n.vsm-mcp-banner{padding:9px 12px;border:1px solid var(--dsw-alias-state-error-secondary,#d9534f55);border-radius:6px;background:var(--dsw-alias-state-error-secondary,#d9534f1a);margin-bottom:10px}\n\n/* 工具 chips */\n.vsm-mcp-tools{display:flex;flex-wrap:wrap;gap:6px}\n.vsm-mcp-chip{font:12px var(--ds-font-family-code,ui-monospace,monospace);padding:4px 8px;border-radius:5px;background:var(--dsw-alias-bg-mask-2,#ffffff14);color:var(--dsw-alias-label-secondary,#c8c8c8);border:1px solid var(--dsw-alias-border-l1,#333)}\n\n/* 空态 / 加载态 */\n.vsm-mcp-empty{text-align:center;padding:56px 20px;color:var(--dsw-alias-label-tertiary,#9aa5b1);border:1px dashed var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;font-size:13px}\n\n/* 项目搜索下拉框 */\n.vsm-project-picker{position:relative;width:100%;z-index:4}\n.vsm-project-picker-trigger{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,#e6e6e6);cursor:pointer;text-align:left}\n.vsm-project-picker-trigger:hover{border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-picker-label{display:flex;flex-direction:column;min-width:0;font-size:13px;font-weight:500}\n.vsm-project-picker-label small{margin-top:3px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-project-picker-arrow{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:16px}\n.vsm-project-picker-menu{position:absolute;top:calc(100% + 6px);left:0;right:0;padding:8px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:9px;background:var(--dsw-alias-bg-overlay,#202020);box-shadow:0 12px 36px #0008}\n.vsm-project-picker-search{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);outline:none}\n.vsm-project-picker-search:focus{border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-picker-options{max-height:260px;overflow:auto;margin-top:6px}\n.vsm-project-option{display:flex;width:100%;flex-direction:column;gap:3px;padding:8px 9px;border:0;border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;text-align:left}\n.vsm-project-option:hover,.vsm-project-option.highlight{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-project-option.selected{color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-project-option small{overflow:hidden;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);text-overflow:ellipsis;white-space:nowrap}\n.vsm-project-picker-empty{padding:18px 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:center}\n\n/* 项目级 MCP 分组 */\n.vsm-project{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;margin-bottom:14px;overflow:hidden;background:var(--dsw-alias-bg-layer-1,transparent)}\n.vsm-project-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:var(--dsw-alias-bg-mask-1,#ffffff0a)}\n.vsm-project-title{display:flex;align-items:center;gap:10px;min-width:0}\n.vsm-project-icon{color:var(--dsw-alias-brand-primary,#6d55ff);font-size:15px;flex-shrink:0}\n.vsm-project-name{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);display:block}\n.vsm-project-path{font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-family:var(--ds-font-family-code,ui-monospace,monospace);display:block;margin-top:2px;word-break:break-all}\n.vsm-primary.vsm-small{padding:5px 12px;font-size:12px}\n.vsm-project-empty{text-align:center;padding:26px 16px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px}\n.vsm-project .vsm-mcp-card{border:0;border-radius:0;margin:0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:transparent}\n.vsm-project .vsm-mcp-card:last-child{border-bottom:0}\n.vsm-project .vsm-mcp-banner{border-radius:0;margin:0}\n\n/* 新增 MCP 弹窗 */\n.vsm-mcp-modal{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,#000000aa);display:flex;align-items:center;justify-content:center;z-index:50}\n.vsm-mcp-dialog{width:min(560px,calc(100vw - 40px));max-height:88vh;overflow:auto;padding:22px 24px;border-radius:12px;background:var(--dsw-alias-bg-overlay,#202020);border:1px solid var(--dsw-alias-border-l2,#444);box-shadow:0 16px 60px #000c;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-mcp-dialog h3{margin:0 0 14px;font-size:16px;font-weight:600}\n.vsm-mcp-dialog label{display:flex;flex-direction:column;gap:6px;margin:12px 0;font-size:12px;color:var(--dsw-alias-label-secondary,#aaa)}\n.vsm-mcp-dialog input,.vsm-mcp-dialog select,.vsm-mcp-dialog textarea{font:inherit;color:var(--dsw-alias-label-primary,#eee);background:var(--dsw-alias-bg-layer-1,#2b2b2b);border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 10px}\n.vsm-mcp-dialog input:focus,.vsm-mcp-dialog select:focus,.vsm-mcp-dialog textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#6d55ff)}\n.vsm-mcp-dialog textarea{min-height:64px;resize:vertical;font-family:var(--ds-font-family-code,ui-monospace,monospace)}\n.vsm-mcp-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}\n.vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}\n.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n\n/* 兼容性条目(面板标题改用 .vsm-panel-title) */\n.vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}\n.vsm-compat-item.ok .vsm-compat-name::before{content:\"● \";color:var(--dsw-alias-state-success-primary,#38bd75)}\n.vsm-compat-item.warn .vsm-compat-name::before{content:\"● \";color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-compat-name{font-weight:500;min-width:0}\n.vsm-compat-note{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:right;min-width:0}\n\n/* 开发形态开关 */\n.vsm-devform-note{display:block;margin-top:4px;font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);word-break:break-all}\n.vsm-devform-actions{display:flex;flex-direction:column;align-items:flex-end;gap:6px;min-width:0}\n.vsm-devform-msg{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px;text-align:right}\n.vsm-general-row button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:6px 12px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}\n.vsm-general-row button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-general-row button:disabled{opacity:.55;cursor:default}\n.vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}\n\n/* keyboard shortcuts page */\n.vsm-kb-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}\n.vsm-kb-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#161616)}\n.vsm-kb-row.rec{border-color:var(--dsw-alias-brand-primary,#4f8cff)}\n.vsm-kb-label{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;color:var(--dsw-alias-label-primary,#eee);font-size:13px}\n.vsm-kb-warn{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px}\n.vsm-kb-key{min-width:96px;text-align:center;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-family:var(--ds-font-family-code,ui-monospace,monospace)}\n.vsm-kb-key.warn{border-color:var(--dsw-alias-state-warn-primary,#e5b93f);color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-kb-key.invalid{border-color:var(--dsw-alias-state-error-primary,#d9534f);color:var(--dsw-alias-state-error-primary,#d9534f)}\n.vsm-kb-rec{font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}\n.vsm-kb-actions{display:flex;gap:6px}\n.vsm-kb-actions button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:4px 10px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}\n.vsm-kb-actions button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-kb-actions button:disabled{opacity:.55;cursor:default}\n.vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}\n.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}\n\n/* 语言服务器(LSP)设置子 Tab */\n.vsm-lsp-form{display:flex;flex-direction:column;gap:6px;margin-top:8px}\n.vsm-lsp-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}\n.vsm-lsp-form input{height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;outline:none}\n.vsm-lsp-form input:focus{border-color:var(--dsw-alias-brand-primary,#4f8cff)}\n.vsm-lsp-form button{align-self:flex-start;margin-top:2px}\n.vsm-lsp-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin-top:10px;line-height:1.6}\n\n/* 会话性能(PerfSettings 子 Tab) */\n.vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}\n.vsm-perf-page>.vsm-panel-body>*:first-child{margin-top:0}\n.vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}\n.vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}\n.vsm-perf-card{display:flex;flex-direction:column;gap:4px;min-width:120px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;background:var(--dsw-alias-bg-layer-2,transparent)}\n.vsm-perf-card span{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-card b{font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}\n.vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}\n/* 工作区列表分页器 */\n.vsm-perf-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:4px}\n.vsm-perf-pager-text{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-pager-actions{display:flex;gap:8px}\n.vsm-perf-ws-list{display:flex;flex-direction:column;gap:8px}\n.vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}\n.vsm-perf-ws-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);cursor:pointer;user-select:none}\n.vsm-perf-ws-head:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-perf-ws-head:focus-visible{outline:1px solid var(--dsw-alias-brand-primary,#6d55ff);outline-offset:-1px}\n.vsm-perf-ws.expanded .vsm-perf-ws-head{border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}\n.vsm-perf-ws-caret{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;flex-shrink:0;transition:transform .15s;display:inline-block}\n.vsm-perf-ws-caret.open{transform:rotate(90deg)}\n.vsm-perf-ws .vsm-perf-ws-name{flex:1}\n.vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}\n.vsm-perf-rows{display:flex;flex-direction:column}\n.vsm-perf-row{display:flex;align-items:center;gap:10px;padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-perf-row:last-child{border-bottom:0}\n.vsm-perf-row:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff0d)}\n.vsm-perf-row input[type=checkbox]{accent-color:var(--dsw-alias-brand-primary,#6d55ff);cursor:pointer}\n.vsm-perf-cell{min-width:0}\n.vsm-perf-id{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,monospace);color:var(--dsw-alias-label-primary,#eee)}\n.vsm-perf-size{width:70px;text-align:right;flex-shrink:0;color:var(--dsw-alias-label-secondary,#c8c8c8)}\n.vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}\n.vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}\n.vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}\n.vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}\n.vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}\n.vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}\n.vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}\n.vsm-perf-modal-card h3{margin:0;font-size:16px}\n.vsm-perf-modal-note{font-size:12px}\n.vsm-perf-modal-list{list-style:none;margin:0;padding:0;overflow-y:auto;font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8);display:flex;flex-direction:column;gap:4px}\n.vsm-perf-modal-list li{padding:4px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#262626);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.vsm-perf-modal-more{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}\n.vsm-perf-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}\n.vsm-secondary{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 16px;background:none;color:var(--dsw-alias-label-primary,#eee);font-size:13px;cursor:pointer;white-space:nowrap}\n.vsm-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}\n.vsm-secondary:disabled{opacity:.55;cursor:default}\n.vsm-small{padding:4px 10px;font-size:12px}\n\n/* 扩展管理(LSP 语言服务器) */\n.vsm-mcp-actions button.vsm-primary,\n.vsm-mcp-actions button.vsm-danger{width:auto;height:auto;padding:5px 12px;font-size:12px;border-radius:6px;line-height:1.4}\n.vsm-mcp-actions button.vsm-danger{color:var(--dsw-alias-state-error-primary,#f07878);background:none}\n.vsm-mcp-actions button.vsm-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}\n.vsm-ext-ver{font-weight:400;font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n";
4234
4905
  const tagId = "dsh-vscode-mode/mcp.css";
4235
4906
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4236
4907
  const tag = document.createElement("style");
@@ -4617,7 +5288,7 @@ window.__ModuleLoader__.load({
4617
5288
  onClick: () => resetRow(command.id)
4618
5289
  }, "恢复"))));
4619
5290
  });
4620
- return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "快捷键"), react.default.createElement("p", null, "配置编辑器视图的快捷键(保存 / 快速打开 / 侧边栏 / 搜索)。修改即时写入配置,重新聚焦编辑器后生效。"), unavailable && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "设置服务暂不可用,当前使用默认键位。"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), message && react.default.createElement("div", { className: "vsm-kb-message" }, message), react.default.createElement("div", { className: "vsm-kb-list" }, rows), react.default.createElement("div", { className: "vsm-kb-actionsbar" }, react.default.createElement("span", { className: "vsm-kb-dirty" }, dirty ? "有未保存的修改" : ""), react.default.createElement("button", {
5291
+ return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "快捷键"), react.default.createElement("p", null, "配置编辑器视图的快捷键(保存 / 快速打开 / 侧边栏 / 搜索)。修改即时写入配置,重新聚焦编辑器后生效。"), unavailable && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "设置服务暂不可用,当前使用默认键位。"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), message && react.default.createElement("div", { className: "vsm-kb-message" }, message), react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "编辑器键位"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-kb-list" }, rows)), react.default.createElement("div", { className: "vsm-panel-body vsm-kb-actionsbar" }, react.default.createElement("span", { className: "vsm-kb-dirty" }, dirty ? "有未保存的修改" : ""), react.default.createElement("button", {
4621
5292
  className: "vsm-primary vsm-small",
4622
5293
  disabled: unavailable || notReady || busy || snapshot?.writable === false || !dirty,
4623
5294
  onClick: save
@@ -4625,7 +5296,7 @@ window.__ModuleLoader__.load({
4625
5296
  className: "vsm-small",
4626
5297
  disabled: unavailable || notReady || busy || snapshot?.writable === false,
4627
5298
  onClick: resetAll
4628
- }, "恢复默认")));
5299
+ }, "恢复默认"))));
4629
5300
  }
4630
5301
  //#endregion
4631
5302
  //#region src/client/ui/LspSettings.ts
@@ -4637,8 +5308,8 @@ window.__ModuleLoader__.load({
4637
5308
  */
4638
5309
  const LANGUAGES = [{
4639
5310
  id: "lua",
4640
- label: "Lua(LuaLS)",
4641
- hint: "lua-language-server,EmmyLua 注解原生支持"
5311
+ label: "Lua(EmmyLua / LuaLS)",
5312
+ hint: "优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server"
4642
5313
  }, {
4643
5314
  id: "csharp",
4644
5315
  label: "C#(Roslyn / OmniSharp)",
@@ -4676,8 +5347,8 @@ window.__ModuleLoader__.load({
4676
5347
  label: "更新"
4677
5348
  }
4678
5349
  ];
4679
- /** 语言卡片:状态 + 启用开关 + 命令/路径配置。 */
4680
- function LangCard({ lang, config, status, busy, onToggle, onSave }) {
5350
+ /** 语言卡片:状态 + 启用开关 + 命令/路径配置 + 重新检测。 */
5351
+ function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect }) {
4681
5352
  const [pathDraft, setPathDraft] = react.default.useState(config?.path ?? "");
4682
5353
  const [commandDraft, setCommandDraft] = react.default.useState(config?.command ?? "");
4683
5354
  react.default.useEffect(() => {
@@ -4689,7 +5360,7 @@ window.__ModuleLoader__.load({
4689
5360
  className: "vsm-switch " + (config?.enabled !== false ? "on" : ""),
4690
5361
  onClick: () => onToggle(lang.id, config?.enabled !== false ? false : true),
4691
5362
  "aria-label": "启用/禁用"
4692
- }, config?.enabled !== false ? "●" : "○"))), react.default.createElement("div", { className: "vsm-mcp-meta" }, PHASE_LABEL[phase] ?? phase, " · ", SOURCE_LABEL[status?.source] ?? status?.source, phase === "ready" && status?.root ? " · " + String(status.root).split(/[\\/]/).pop() : ""), status?.reason ? react.default.createElement("div", { className: "vsm-mcp-error" }, status.reason) : null, react.default.createElement("div", { className: "vsm-lsp-form" }, react.default.createElement("label", null, react.default.createElement("span", null, "可执行文件路径(绝对路径,优先)"), react.default.createElement("input", {
5363
+ }, config?.enabled !== false ? "●" : "○"))), react.default.createElement("div", { className: "vsm-mcp-meta" }, PHASE_LABEL[phase] ?? phase, " · ", SOURCE_LABEL[status?.source] ?? status?.source, status?.providerName ? " · " + status.providerName : "", status?.version ? " · v" + status.version : "", phase === "ready" && status?.root ? " · " + String(status.root).split(/[\\/]/).pop() : ""), status?.reason ? react.default.createElement("div", { className: "vsm-mcp-error" }, status.reason) : null, react.default.createElement("div", { className: "vsm-lsp-form" }, react.default.createElement("label", null, react.default.createElement("span", null, "可执行文件路径(绝对路径,优先)"), react.default.createElement("input", {
4693
5364
  value: pathDraft,
4694
5365
  placeholder: "如 C:/.../lua-language-server.exe",
4695
5366
  onChange: (e) => setPathDraft(e.target.value)
@@ -4701,7 +5372,12 @@ window.__ModuleLoader__.load({
4701
5372
  className: "vsm-primary",
4702
5373
  disabled: busy === lang.id,
4703
5374
  onClick: () => onSave(lang.id, pathDraft, commandDraft)
4704
- }, busy === lang.id ? "保存中…" : "保存配置")), react.default.createElement("div", { className: "vsm-lsp-hint" }, lang.hint));
5375
+ }, busy === lang.id ? "保存中…" : "保存配置"), react.default.createElement("button", {
5376
+ className: "vsm-primary vsm-small",
5377
+ disabled: busy === lang.id,
5378
+ onClick: () => onRedetect(lang.id),
5379
+ title: "清缓存并重新选择语言服务器(EmmyLua / LuaLS)"
5380
+ }, busy === lang.id ? "检测中…" : "重新检测")), react.default.createElement("div", { className: "vsm-lsp-hint" }, lang.hint));
4705
5381
  }
4706
5382
  /** 已装扩展卡片:清单 + 卸载/更新。 */
4707
5383
  function InstalledCard({ ext, busy, onUninstall, onUpdate }) {
@@ -4768,6 +5444,14 @@ window.__ModuleLoader__.load({
4768
5444
  if (next === "installed" || next === "updates") refreshExt();
4769
5445
  if (next === "servers") refreshServers();
4770
5446
  };
5447
+ /** 合并 redetect/configUpdate 返回的服务器状态:只替换该语言条目。 */
5448
+ const mergeServers = (languageId, list) => {
5449
+ setServers((prev) => Array.isArray(list) ? [...prev.filter((s) => s.languageId !== languageId), ...list] : prev);
5450
+ };
5451
+ /** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
5452
+ const notifyResync = (languageId) => {
5453
+ window.dispatchEvent(new CustomEvent("edrv:lsp-redetect", { detail: { languageId } }));
5454
+ };
4771
5455
  const saveLang = (languageId, path, command) => {
4772
5456
  setBusy(languageId);
4773
5457
  setError("");
@@ -4781,6 +5465,8 @@ window.__ModuleLoader__.load({
4781
5465
  return;
4782
5466
  }
4783
5467
  setConfig(res.config ?? {});
5468
+ mergeServers(languageId, res.servers);
5469
+ notifyResync(languageId);
4784
5470
  }).catch((e) => setError(String(e))).finally(() => setBusy(""));
4785
5471
  };
4786
5472
  const toggleLang = (languageId, enabled) => {
@@ -4789,10 +5475,25 @@ window.__ModuleLoader__.load({
4789
5475
  languageId,
4790
5476
  enabled
4791
5477
  }).then((res) => {
4792
- if (res?.ok) {
4793
- setConfig(res.config ?? {});
4794
- refreshServers();
4795
- } else setError(res?.error ?? "切换失败");
5478
+ if (!res?.ok) {
5479
+ setError(res?.error ?? "切换失败");
5480
+ return;
5481
+ }
5482
+ setConfig(res.config ?? {});
5483
+ mergeServers(languageId, res.servers);
5484
+ notifyResync(languageId);
5485
+ }).catch((e) => setError(String(e))).finally(() => setBusy(""));
5486
+ };
5487
+ const redetectLang = (languageId) => {
5488
+ setBusy(languageId);
5489
+ setError("");
5490
+ rpc("edrv.lsp.redetect", { languageId }).then((res) => {
5491
+ if (!res?.ok) {
5492
+ setError(res?.error ?? "重新检测失败");
5493
+ return;
5494
+ }
5495
+ mergeServers(languageId, res.servers);
5496
+ notifyResync(languageId);
4796
5497
  }).catch((e) => setError(String(e))).finally(() => setBusy(""));
4797
5498
  };
4798
5499
  const searchMarket = () => {
@@ -4862,24 +5563,27 @@ window.__ModuleLoader__.load({
4862
5563
  onClick: refresh
4863
5564
  }, "↻ 刷新"));
4864
5565
  const banner = error ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error) : null;
5566
+ /** 线框面板容器:标题 + 内容体。 */
5567
+ const panel = (title, children) => react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, title), react.default.createElement("div", { className: "vsm-panel-body" }, children));
4865
5568
  let body = null;
4866
- if (tab === "servers") body = react.default.createElement("div", null, LANGUAGES.map((lang) => react.default.createElement(LangCard, {
5569
+ if (tab === "servers") body = panel("语言服务器", react.default.createElement(react.default.Fragment, null, LANGUAGES.map((lang) => react.default.createElement(LangCard, {
4867
5570
  key: lang.id,
4868
5571
  lang,
4869
5572
  config: config[lang.id] ?? {},
4870
5573
  status: servers.find((s) => s.languageId === lang.id),
4871
5574
  busy,
4872
5575
  onToggle: toggleLang,
4873
- onSave: saveLang
4874
- })), react.default.createElement("div", { className: "vsm-lsp-hint" }, "提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。"));
4875
- else if (tab === "installed") body = react.default.createElement("div", null, react.default.createElement("div", { className: "vsm-lsp-hint" }, "已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。"), installed.length ? installed.map((ext) => react.default.createElement(InstalledCard, {
5576
+ onSave: saveLang,
5577
+ onRedetect: redetectLang
5578
+ })), react.default.createElement("div", { className: "vsm-lsp-hint" }, "提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。")));
5579
+ else if (tab === "installed") body = panel("已安装扩展", react.default.createElement(react.default.Fragment, null, react.default.createElement("div", { className: "vsm-lsp-hint" }, "已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。"), installed.length ? installed.map((ext) => react.default.createElement(InstalledCard, {
4876
5580
  key: ext.id,
4877
5581
  ext,
4878
5582
  busy,
4879
5583
  onUninstall: uninstallExt,
4880
5584
  onUpdate: updateExt
4881
- })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有安装扩展(去「市场」或指定本地 .vsix)"));
4882
- else if (tab === "market") body = react.default.createElement("div", null, react.default.createElement("div", { className: "vsm-lsp-form" }, react.default.createElement("label", null, react.default.createElement("span", null, "搜索 Open VSX 市场"), react.default.createElement("input", {
5585
+ })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有安装扩展(去「市场」或指定本地 .vsix)")));
5586
+ else if (tab === "market") body = panel("扩展市场", react.default.createElement(react.default.Fragment, null, react.default.createElement("div", { className: "vsm-lsp-form" }, react.default.createElement("label", null, react.default.createElement("span", null, "搜索 Open VSX 市场"), react.default.createElement("input", {
4883
5587
  value: query,
4884
5588
  placeholder: "如 sumneko.lua / lua language server",
4885
5589
  onChange: (e) => setQuery(e.target.value),
@@ -4904,18 +5608,107 @@ window.__ModuleLoader__.load({
4904
5608
  installed,
4905
5609
  busy,
4906
5610
  onInstall: installMarket
4907
- })));
4908
- else body = react.default.createElement("div", null, react.default.createElement("div", { className: "vsm-lsp-hint" }, "已装扩展与 Open VSX 最新版的差异。"), updates.length ? updates.map((u) => react.default.createElement("article", {
5611
+ }))));
5612
+ else body = panel("可用更新", react.default.createElement(react.default.Fragment, null, react.default.createElement("div", { className: "vsm-lsp-hint" }, "已装扩展与 Open VSX 最新版的差异。"), updates.length ? updates.map((u) => react.default.createElement("article", {
4909
5613
  key: u.id,
4910
5614
  className: "vsm-mcp-card"
4911
5615
  }, react.default.createElement("div", { className: "vsm-mcp-card-head" }, react.default.createElement("div", { className: "vsm-mcp-title" }, react.default.createElement("span", { className: "vsm-mcp-dot error" }), react.default.createElement("span", null, u.displayName || u.id), react.default.createElement("span", { className: "vsm-ext-ver" }, "v" + u.current + " → v" + u.latest)), react.default.createElement("div", { className: "vsm-mcp-actions" }, react.default.createElement("button", {
4912
5616
  className: "vsm-primary vsm-small",
4913
5617
  disabled: busy === u.id,
4914
5618
  onClick: () => updateExt(u)
4915
- }, busy === u.id ? "更新中…" : "更新"))))) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "全部已是最新"));
5619
+ }, busy === u.id ? "更新中…" : "更新"))))) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "全部已是最新")));
4916
5620
  return react.default.createElement("div", null, header, nav, banner, body);
4917
5621
  }
4918
5622
  //#endregion
5623
+ //#region src/client/state/workspaceFoldCache.ts
5624
+ /**
5625
+ * dsh-vscode-mode client — 「性能优化」页工作区栏目展开状态持久化(纯函数,可单测)。
5626
+ * 只持久化「已展开」的工作区键集合(默认全部折叠),对齐 explorerCache 的做法:
5627
+ * 不缓存会话条目本身,条目由 edrv.perf.inventory 实时返回。
5628
+ * 键前缀统一走 paths.ts PathConst(CACHE_KEY.workspaceFold)。
5629
+ * 作者 ddj 2026-09-02
5630
+ */
5631
+ /** localStorage 键(全局,不按会话隔离)。 */
5632
+ const STORE_KEY = CACHE_KEY.workspaceFold;
5633
+ /**
5634
+ * 归一化工作区键:仅去首尾空白(工作区键非文件系统路径,不做斜杠转换)。
5635
+ * @author ddj 2026年09月02号
5636
+ * @param key 工作区标识
5637
+ * @returns 归一化键,空值返回 ''
5638
+ */
5639
+ function normalizeFoldKey(key) {
5640
+ return String(key ?? "").trim();
5641
+ }
5642
+ /**
5643
+ * 裁剪展开键集:去重、丢弃空值、超容量丢弃尾部(保持传入顺序)。
5644
+ * @author ddj 2026年09月02号
5645
+ * @param expanded 原始展开键集合
5646
+ * @returns 规范化后的键数组
5647
+ */
5648
+ function sanitizeFolded(expanded) {
5649
+ if (!Array.isArray(expanded)) return [];
5650
+ const seen = /* @__PURE__ */ new Set();
5651
+ const out = [];
5652
+ for (const item of expanded) {
5653
+ const key = normalizeFoldKey(typeof item === "string" ? item : "");
5654
+ if (!key || seen.has(key)) continue;
5655
+ seen.add(key);
5656
+ out.push(key);
5657
+ if (out.length >= 300) break;
5658
+ }
5659
+ return out;
5660
+ }
5661
+ /**
5662
+ * 序列化展开状态(JSON,带 schema 版本号)。
5663
+ * @author ddj 2026年09月02号
5664
+ * @param expanded 已展开的项目键集合
5665
+ * @returns JSON 字符串
5666
+ */
5667
+ function serializeFold(expanded) {
5668
+ return JSON.stringify({
5669
+ v: 1,
5670
+ expanded: sanitizeFolded(expanded)
5671
+ });
5672
+ }
5673
+ /**
5674
+ * 解析缓存文本;损坏/格式不符 → 空集合。
5675
+ * @author ddj 2026年09月02号
5676
+ * @param text localStorage 原文
5677
+ * @returns 已展开键集合
5678
+ */
5679
+ function parseFold(text) {
5680
+ if (!text) return [];
5681
+ try {
5682
+ const data = JSON.parse(text);
5683
+ if (!data || typeof data !== "object") return [];
5684
+ return sanitizeFolded(data.expanded);
5685
+ } catch (error) {
5686
+ return [];
5687
+ }
5688
+ }
5689
+ /**
5690
+ * 读取展开状态(localStorage 不可用 → 空集合)。
5691
+ * @author ddj 2026年09月02号
5692
+ * @returns 已展开键集合
5693
+ */
5694
+ function foldLoad() {
5695
+ try {
5696
+ return parseFold(window.localStorage.getItem(STORE_KEY));
5697
+ } catch (error) {
5698
+ return [];
5699
+ }
5700
+ }
5701
+ /**
5702
+ * 写入展开状态(配额满/隐私模式失败静默)。
5703
+ * @author ddj 2026年09月02号
5704
+ * @param expanded 已展开键集合
5705
+ */
5706
+ function foldSave(expanded) {
5707
+ try {
5708
+ window.localStorage.setItem(STORE_KEY, serializeFold(expanded));
5709
+ } catch (error) {}
5710
+ }
5711
+ //#endregion
4919
5712
  //#region src/client/ui/PerfSettings.ts
4920
5713
  /**
4921
5714
  * dsh-vscode-mode client — PerfSettings:「性能优化」设置子页(会话卫生 + 压缩调优)。
@@ -4935,6 +5728,43 @@ window.__ModuleLoader__.load({
4935
5728
  }
4936
5729
  /** 会话行 key:workspaceKey \u0000 sessionId。 */
4937
5730
  const keyOf = (ws, id) => ws + "\0" + id;
5731
+ /** 工作区列表每页条数。 */
5732
+ const PAGE_SIZE = 7;
5733
+ /**
5734
+ * 计算分页(纯函数,可单测)。
5735
+ * 作者 ddj 2026年09月02号
5736
+ * @param total 总条数
5737
+ * @param pageSize 每页条数
5738
+ * @param page 当前页码(1 起)
5739
+ * @returns 归一化后的分页信息(page 被夹到有效范围)
5740
+ */
5741
+ function paginate(total, pageSize, page) {
5742
+ const size = Math.max(1, Math.floor(pageSize) || 1);
5743
+ const pages = Math.max(1, Math.ceil(total / size));
5744
+ const current = Math.min(Math.max(1, Math.floor(page) || 1), pages);
5745
+ const start = (current - 1) * size;
5746
+ return {
5747
+ page: current,
5748
+ pages,
5749
+ pageSize: size,
5750
+ start,
5751
+ end: Math.min(start + size, total),
5752
+ total
5753
+ };
5754
+ }
5755
+ /**
5756
+ * 取当前页切片(纯函数,可单测)。
5757
+ * 作者 ddj 2026年09月02号
5758
+ * @param items 全量列表
5759
+ * @param page 当前页码(1 起)
5760
+ * @param pageSize 每页条数
5761
+ * @returns 当前页子数组
5762
+ */
5763
+ function pageSlice(items, page, pageSize) {
5764
+ const list = items ?? [];
5765
+ const { start, end } = paginate(list.length, pageSize, page);
5766
+ return list.slice(start, end);
5767
+ }
4938
5768
  /** 单行会话。 */
4939
5769
  function SessionRow({ row, checked, disabled, onToggle }) {
4940
5770
  const age = Math.max(0, (Date.now() - row.mtime) / 864e5);
@@ -4953,6 +5783,63 @@ window.__ModuleLoader__.load({
4953
5783
  }, when), row.active ? react.default.createElement("span", { className: "vsm-perf-cell vsm-perf-active" }, "活跃") : null);
4954
5784
  }
4955
5785
  /**
5786
+ * 工作区列表分页器:顶部文案 + 上一页/下一页交互。
5787
+ * 作者 ddj 2026年09月02号
5788
+ * @param {object} info paginate() 的分页信息
5789
+ * @param {Function} onPrev 上一页回调
5790
+ * @param {Function} onNext 下一页回调
5791
+ */
5792
+ function WorkspacePager({ info, onPrev, onNext }) {
5793
+ const { page, pages, start, end, total } = info;
5794
+ return react.default.createElement("div", { className: "vsm-perf-pager" }, react.default.createElement("span", { className: "vsm-perf-pager-text" }, total ? `第 ${start + 1}–${end} 个 / 共 ${total} 个工作区(第 ${page}/${pages} 页)` : "暂无工作区"), react.default.createElement("div", { className: "vsm-perf-pager-actions" }, react.default.createElement("button", {
5795
+ className: "vsm-secondary vsm-small",
5796
+ onClick: onPrev,
5797
+ disabled: page <= 1
5798
+ }, "‹ 上一页"), react.default.createElement("button", {
5799
+ className: "vsm-secondary vsm-small",
5800
+ onClick: onNext,
5801
+ disabled: page >= pages
5802
+ }, "下一页 ›")));
5803
+ }
5804
+ /**
5805
+ * 工作区栏目(可展开/折叠),内含该工作区的会话行。
5806
+ * 作者 ddj 2026年09月02号
5807
+ * @param {object} ws 工作区汇总(workspaceKey / totalBytes)
5808
+ * @param {object[]} rows 该工作区的会话行
5809
+ * @param {boolean} expanded 是否展开
5810
+ * @param {Function} onToggleFold 展开/折叠切换回调
5811
+ * @param {object} selected 已选会话键集合
5812
+ * @param {boolean} locked 会话勾选是否禁用(移出执行中)
5813
+ * @param {Function} onToggleRow 单行勾选回调
5814
+ */
5815
+ function WorkspaceBlock({ ws, rows, expanded, onToggleFold, selected, locked, onToggleRow }) {
5816
+ const head = react.default.createElement("div", {
5817
+ className: "vsm-perf-ws-head",
5818
+ role: "button",
5819
+ tabIndex: 0,
5820
+ "aria-expanded": expanded,
5821
+ onClick: onToggleFold,
5822
+ onKeyDown: (event) => {
5823
+ if (event.key === "Enter" || event.key === " ") {
5824
+ event.preventDefault();
5825
+ onToggleFold();
5826
+ }
5827
+ }
5828
+ }, react.default.createElement("span", { className: "vsm-perf-ws-caret " + (expanded ? "open" : "") }, "▸"), react.default.createElement("span", {
5829
+ className: "vsm-perf-ws-name",
5830
+ title: ws.workspaceKey
5831
+ }, ws.workspaceKey), react.default.createElement("span", { className: "vsm-perf-ws-meta" }, rows.length + " 会话 · " + formatBytes(ws.totalBytes)));
5832
+ if (!expanded) return react.default.createElement("div", { className: "vsm-perf-ws" }, head);
5833
+ const body = rows.length ? rows.map((row) => react.default.createElement(SessionRow, {
5834
+ key: keyOf(row.workspaceKey, row.sessionId),
5835
+ row,
5836
+ checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
5837
+ disabled: row.active || locked,
5838
+ onToggle: () => onToggleRow(keyOf(row.workspaceKey, row.sessionId))
5839
+ })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "无会话");
5840
+ return react.default.createElement("div", { className: "vsm-perf-ws expanded" }, head, react.default.createElement("div", { className: "vsm-perf-rows" }, body));
5841
+ }
5842
+ /**
4956
5843
  * 会话卫生 + 压缩调优设置子页。
4957
5844
  * @author ddj 2026年09月02号
4958
5845
  */
@@ -4964,6 +5851,26 @@ window.__ModuleLoader__.load({
4964
5851
  const [confirm, setConfirm] = react.default.useState(null);
4965
5852
  const [moved, setMoved] = react.default.useState(null);
4966
5853
  const [config, setConfig] = react.default.useState(null);
5854
+ const [expanded, setExpanded] = react.default.useState(() => foldLoad());
5855
+ const [page, setPage] = react.default.useState(1);
5856
+ react.default.useEffect(() => {
5857
+ foldSave(expanded);
5858
+ }, [expanded]);
5859
+ /** 数据变化后把越界页码夹回有效范围。 */
5860
+ const workspaces = inv?.workspaces ?? [];
5861
+ const pageInfo = paginate(workspaces.length, PAGE_SIZE, page);
5862
+ react.default.useEffect(() => {
5863
+ if (page !== pageInfo.page) setPage(pageInfo.page);
5864
+ }, [page, pageInfo.page]);
5865
+ /** 切换某工作区栏目的展开/折叠(默认全折叠)。 */
5866
+ const toggleFold = (key) => setExpanded((old) => old.includes(key) ? old.filter((item) => item !== key) : old.concat(key));
5867
+ /** 切换单会话勾选。 */
5868
+ const toggleRow = (k) => setSelected((old) => {
5869
+ const next = { ...old };
5870
+ if (next[k]) delete next[k];
5871
+ else next[k] = true;
5872
+ return next;
5873
+ });
4967
5874
  const load = react.default.useCallback(() => {
4968
5875
  setError("");
4969
5876
  Promise.all([rpc("edrv.perf.inventory", {}), rpc("edrv.perf.configGet", {})]).then(([i, c]) => {
@@ -5066,30 +5973,25 @@ window.__ModuleLoader__.load({
5066
5973
  };
5067
5974
  const totals = inv?.totals;
5068
5975
  const summaryCards = totals ? react.default.createElement("div", { className: "vsm-perf-cards" }, react.default.createElement("div", { className: "vsm-perf-card" }, react.default.createElement("span", null, "工作区"), react.default.createElement("b", null, String(totals.workspaces))), react.default.createElement("div", { className: "vsm-perf-card" }, react.default.createElement("span", null, "会话数"), react.default.createElement("b", null, String(totals.sessions))), react.default.createElement("div", { className: "vsm-perf-card" }, react.default.createElement("span", null, "压缩态总体积"), react.default.createElement("b", { className: totals.totalBytes >= 52428800 ? "big" : "" }, formatBytes(totals.totalBytes)))) : null;
5069
- const workspaceBlocks = inv?.workspaces ? inv.workspaces.map((ws) => {
5070
- const rows = inv.sessions.filter((s) => s.workspaceKey === ws.workspaceKey);
5071
- return react.default.createElement("div", {
5072
- key: ws.workspaceKey,
5073
- className: "vsm-perf-ws"
5074
- }, react.default.createElement("div", { className: "vsm-perf-ws-head" }, react.default.createElement("span", {
5075
- className: "vsm-perf-ws-name",
5076
- title: ws.workspaceKey
5077
- }, ws.workspaceKey), react.default.createElement("span", { className: "vsm-perf-ws-meta" }, rows.length + " 会话 · " + formatBytes(ws.totalBytes))), react.default.createElement("div", { className: "vsm-perf-rows" }, rows.length ? rows.map((row) => react.default.createElement(SessionRow, {
5078
- key: keyOf(row.workspaceKey, row.sessionId),
5079
- row,
5080
- checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
5081
- disabled: row.active || busy === "move",
5082
- onToggle: () => {
5083
- const k = keyOf(row.workspaceKey, row.sessionId);
5084
- setSelected((old) => {
5085
- const next = { ...old };
5086
- if (next[k]) delete next[k];
5087
- else next[k] = true;
5088
- return next;
5089
- });
5090
- }
5091
- })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "无会话")));
5976
+ const visible = pageSlice(workspaces, pageInfo.page, PAGE_SIZE);
5977
+ const workspacePager = workspaces.length > PAGE_SIZE ? react.default.createElement(WorkspacePager, {
5978
+ info: pageInfo,
5979
+ onPrev: () => setPage((old) => Math.max(1, old - 1)),
5980
+ onNext: () => setPage((old) => Math.min(pageInfo.pages, old + 1))
5092
5981
  }) : null;
5982
+ const workspaceBlocks = workspaces.length ? react.default.createElement("div", { className: "vsm-perf-ws-list" }, visible.map((ws) => {
5983
+ const key = normalizeFoldKey(ws.workspaceKey);
5984
+ return react.default.createElement(WorkspaceBlock, {
5985
+ key: ws.workspaceKey,
5986
+ ws,
5987
+ rows: inv.sessions.filter((s) => s.workspaceKey === ws.workspaceKey),
5988
+ expanded: expanded.includes(key),
5989
+ onToggleFold: () => toggleFold(key),
5990
+ selected,
5991
+ locked: busy === "move",
5992
+ onToggleRow: toggleRow
5993
+ });
5994
+ })) : null;
5093
5995
  const selCount = Object.keys(selected).length;
5094
5996
  const confirmBody = confirm ? react.default.createElement("div", { className: "vsm-perf-modal" }, react.default.createElement("div", { className: "vsm-perf-modal-card" }, react.default.createElement("h3", null, "确认移出 " + confirm.items.length + " 个会话?"), react.default.createElement("p", { className: "vsm-perf-modal-note" }, "将从 ~/.dsh/sessions 移出到 ~/.dsh/sessions-archive(可逆,重启后不再回放,释放 " + formatBytes(confirm.reclaimedBytes) + ")。活跃会话已排除。"), react.default.createElement("ul", { className: "vsm-perf-modal-list" }, confirm.items.slice(0, 20).map((item) => react.default.createElement("li", { key: item.workspaceKey + item.sessionId }, item.workspaceKey + " / " + item.sessionId + "(" + formatBytes(item.bytes) + ")"))), confirm.items.length > 20 ? react.default.createElement("div", { className: "vsm-perf-modal-more" }, "…等 " + confirm.items.length + " 个") : null, react.default.createElement("div", { className: "vsm-perf-modal-actions" }, react.default.createElement("button", {
5095
5997
  className: "vsm-primary",
@@ -5099,15 +6001,15 @@ window.__ModuleLoader__.load({
5099
6001
  className: "vsm-secondary",
5100
6002
  onClick: () => setConfirm(null)
5101
6003
  }, "取消")))) : null;
5102
- const movedBody = moved?.moved?.length ? react.default.createElement("div", { className: "vsm-perf-moved" }, react.default.createElement("h3", null, "已移出 " + moved.moved.length + " 个会话(释放 " + formatBytes(moved.reclaimedBytes) + ")"), react.default.createElement("div", { className: "vsm-perf-rows" }, moved.moved.map((item) => react.default.createElement("div", {
6004
+ const movedBody = moved?.moved?.length ? react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "已移出 " + moved.moved.length + " 个会话(释放 " + formatBytes(moved.reclaimedBytes) + ")"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-perf-rows" }, moved.moved.map((item) => react.default.createElement("div", {
5103
6005
  key: item.workspaceKey + item.sessionId,
5104
6006
  className: "vsm-perf-row"
5105
6007
  }, react.default.createElement("span", { className: "vsm-perf-cell vsm-perf-id" }, item.sessionId), react.default.createElement("span", { className: "vsm-perf-cell vsm-perf-size" }, formatBytes(item.bytes)), react.default.createElement("button", {
5106
6008
  className: "vsm-secondary vsm-small",
5107
6009
  onClick: () => doRestore(item),
5108
6010
  disabled: busy === "restore"
5109
- }, "恢复")))), moved.failures?.length ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "失败:" + moved.failures.join(";")) : null) : null;
5110
- const configBody = config ? react.default.createElement("div", { className: "vsm-perf-config" }, react.default.createElement("h3", null, "DSH 内置压缩调优"), react.default.createElement("p", null, "调低 compaction-basic / tool-result-pruner 触发阈值,让长循环会话更早压缩,降低模型可见上下文压力(减少 token 与防 context overflow)。"), react.default.createElement("p", { className: "vsm-perf-note" }, "注意:压缩只追加 compaction 事件,不重写已持久化会话日志(append-only),因此不会缩小已存在的会话文件——存量瘦身请用上方「移出到归档」。packChunks 已默认开启,无需配置。"), react.default.createElement("div", { className: "vsm-perf-config-state" }, react.default.createElement("span", { className: "vsm-perf-cell" }, "状态:" + (config.applied ? "已应用" : "未应用")), config.applied ? react.default.createElement("button", {
6011
+ }, "恢复")))), moved.failures?.length ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "失败:" + moved.failures.join(";")) : null)) : null;
6012
+ const configBody = config ? react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "DSH 内置压缩调优"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("p", null, "调低 compaction-basic / tool-result-pruner 触发阈值,让长循环会话更早压缩,降低模型可见上下文压力(减少 token 与防 context overflow)。"), react.default.createElement("p", { className: "vsm-perf-note" }, "注意:压缩只追加 compaction 事件,不重写已持久化会话日志(append-only),因此不会缩小已存在的会话文件——存量瘦身请用上方「移出到归档」。packChunks 已默认开启,无需配置。"), react.default.createElement("div", { className: "vsm-perf-config-state" }, react.default.createElement("span", { className: "vsm-perf-cell" }, "状态:" + (config.applied ? "已应用" : "未应用")), config.applied ? react.default.createElement("button", {
5111
6013
  className: "vsm-secondary vsm-small",
5112
6014
  onClick: undoConfig,
5113
6015
  disabled: busy === "undo"
@@ -5115,9 +6017,9 @@ window.__ModuleLoader__.load({
5115
6017
  className: "vsm-primary vsm-small",
5116
6018
  onClick: applyConfig,
5117
6019
  disabled: busy === "apply"
5118
- }, "应用建议配置(需重启)")), react.default.createElement("pre", { className: "vsm-perf-block" }, config.block)) : null;
5119
- const guideBody = react.default.createElement("div", { className: "vsm-perf-guide" }, react.default.createElement("h3", null, "侧车摘要(agent/脚本取关键字段,不整份读 sidecar)"), react.default.createElement("p", null, "工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:"), react.default.createElement("pre", { className: "vsm-perf-block" }, "curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' -d '{\"method\":\"edrv.perf.sidecarSummary\",\"args\":{}}'\n# 差异记录列表(含每文件 pending 摘要):edrv.list\n# 历史归档:edrv.archiveList / edrv.archiveRead"));
5120
- return react.default.createElement("section", { className: "vsm-perf-page" }, react.default.createElement("h2", null, "会话性能"), react.default.createElement("p", null, "DSH 启动会回放 ~/.dsh/sessions 全部会话(V8 展开约 10×),体积越大内存越吃紧。定期把巨型/旧会话移出到归档可显著降低启动内存。"), error ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error) : null, summaryCards, react.default.createElement("div", { className: "vsm-perf-toolbar" }, react.default.createElement("button", {
6020
+ }, "应用建议配置(需重启)")), react.default.createElement("pre", { className: "vsm-perf-block" }, config.block))) : null;
6021
+ const guideBody = react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "侧车摘要(agent/脚本取关键字段,不整份读 sidecar)"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("p", null, "工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:"), react.default.createElement("pre", { className: "vsm-perf-block" }, "curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' -d '{\"method\":\"edrv.perf.sidecarSummary\",\"args\":{}}'\n# 差异记录列表(含每文件 pending 摘要):edrv.list\n# 历史归档:edrv.archiveList / edrv.archiveRead")));
6022
+ return react.default.createElement("section", { className: "vsm-perf-page" }, react.default.createElement("h2", null, "会话性能"), react.default.createElement("p", null, "DSH 启动会回放 ~/.dsh/sessions 全部会话(V8 展开约 10×),体积越大内存越吃紧。定期把巨型/旧会话移出到归档可显著降低启动内存。"), error ? react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error) : null, summaryCards, react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "会话盘点"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("div", { className: "vsm-perf-toolbar" }, react.default.createElement("button", {
5121
6023
  className: "vsm-secondary vsm-small",
5122
6024
  onClick: selectBig
5123
6025
  }, "圈选 ≥1MB 非活跃"), react.default.createElement("button", {
@@ -5136,15 +6038,14 @@ window.__ModuleLoader__.load({
5136
6038
  className: "vsm-secondary vsm-small",
5137
6039
  onClick: load,
5138
6040
  disabled: busy === "load"
5139
- }, "⟳ 刷新")), workspaceBlocks, movedBody, configBody, guideBody, confirmBody);
6041
+ }, "⟳ 刷新")), workspacePager, workspaceBlocks)), movedBody, configBody, guideBody, confirmBody);
5140
6042
  }
5141
6043
  //#endregion
5142
6044
  //#region src/client/ui/McpSettings.ts
5143
6045
  /**
5144
- * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理。
5145
- * 子 Tab:通用(文件打开工具/开发形态)/ 快捷键(编辑器键位配置,可重置)/
5146
- * 我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)/ 兼容性。
5147
- * 作者 ddj 2026年08月22号 / 2026年08月26号
6046
+ * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
6047
+ * MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
6048
+ * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
5148
6049
  */
5149
6050
  const EMPTY = {
5150
6051
  serverName: "",
@@ -5357,14 +6258,14 @@ window.__ModuleLoader__.load({
5357
6258
  disabled: devBusy,
5358
6259
  onClick: closeDevForm
5359
6260
  }, devBusy ? "切换中…" : "关闭开发形态"))) : null;
5360
- return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "通用"), react.default.createElement("p", null, "配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。"), unavailable && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "设置服务暂不可用,当前使用自动选择。"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), react.default.createElement("label", { className: "vsm-general-row" }, react.default.createElement("span", null, "文件链接使用工具"), react.default.createElement("select", {
6261
+ return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "通用"), react.default.createElement("p", null, "配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。"), unavailable && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, "设置服务暂不可用,当前使用自动选择。"), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, "文件与安装"), react.default.createElement("div", { className: "vsm-panel-body" }, react.default.createElement("label", { className: "vsm-general-row" }, react.default.createElement("span", null, "文件链接使用工具"), react.default.createElement("select", {
5361
6262
  value: currentOption,
5362
6263
  disabled: loading || unavailable || notReady || busy || snapshot?.writable === false,
5363
6264
  onChange: (event) => save(event.target.value)
5364
6265
  }, options.map((item) => react.default.createElement("option", {
5365
6266
  key: item.id,
5366
6267
  value: item.id
5367
- }, item.label))), loading && react.default.createElement("small", null, "读取中…")), devFormRow);
6268
+ }, item.label))), loading && react.default.createElement("small", null, "读取中…")), devFormRow)));
5368
6269
  }
5369
6270
  /** 兼容性小节:host 报告(RPC compat)+ 客户端本地适配项。 */
5370
6271
  function CompatSection({ getSummary }) {
@@ -5387,12 +6288,111 @@ window.__ModuleLoader__.load({
5387
6288
  key: item.name,
5388
6289
  className: "vsm-compat-item " + (item.active ? "ok" : "warn")
5389
6290
  }, react.default.createElement("span", { className: "vsm-compat-name" }, item.name), react.default.createElement("span", { className: "vsm-compat-note" }, item.note ?? (item.active ? "正常" : "未生效"))));
6291
+ /** 兼容性分组面板:标题 + 线框内容区。 */
6292
+ const group = (title, children) => react.default.createElement("section", { className: "vsm-panel" }, react.default.createElement("h3", { className: "vsm-panel-title" }, title), react.default.createElement("div", { className: "vsm-panel-body" }, children));
5390
6293
  const warnings = report?.warnings ?? [];
5391
6294
  const devForm = report?.devForm;
5392
- return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "兼容性"), react.default.createElement("p", null, "检测与其他插件 / DSH 版本的适配状态与已知问题。" + (report?.pluginVersion ? "(插件版本 " + report.pluginVersion + ")" : "")), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), devForm?.enabled && react.default.createElement(react.default.Fragment, null, react.default.createElement("h3", { className: "vsm-compat-h3" }, "开发形态"), react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "开发形态开启(工作区链接安装)"), react.default.createElement("span", { className: "vsm-compat-note" }, devForm.path))), react.default.createElement("h3", { className: "vsm-compat-h3" }, "外部插件与服务"), renderItems(report?.external), react.default.createElement("h3", { className: "vsm-compat-h3" }, "客户端适配"), renderItems(getSummary?.() ?? []), react.default.createElement("h3", { className: "vsm-compat-h3" }, "护栏"), renderItems(report?.guards), react.default.createElement("h3", { className: "vsm-compat-h3" }, "警告"), warnings.length ? warnings.map((w, index) => react.default.createElement("div", {
6295
+ return react.default.createElement("section", { className: "vsm-general-page" }, react.default.createElement("h2", null, "兼容性"), react.default.createElement("p", null, "检测与其他插件 / DSH 版本的适配状态与已知问题。" + (report?.pluginVersion ? "(插件版本 " + report.pluginVersion + ")" : "")), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), devForm?.enabled && group("开发形态", react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "开发形态开启(工作区链接安装)"), react.default.createElement("span", { className: "vsm-compat-note" }, devForm.path))), group("外部插件与服务", renderItems(report?.external)), group("客户端适配", renderItems(getSummary?.() ?? [])), group("护栏", renderItems(report?.guards)), group("警告", warnings.length ? warnings.map((w, index) => react.default.createElement("div", {
5393
6296
  key: index,
5394
6297
  className: "vsm-mcp-error vsm-mcp-banner"
5395
- }, w)) : react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "未检测到兼容性问题"), react.default.createElement("span", { className: "vsm-compat-note" }, "")));
6298
+ }, w)) : react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, "未检测到兼容性问题"), react.default.createElement("span", { className: "vsm-compat-note" }, ""))));
6299
+ }
6300
+ /** MCP 管理子页签定义(我的 / 项目 / 市场)。 */
6301
+ const MCP_TABS = [
6302
+ {
6303
+ id: "mine",
6304
+ label: "我的 MCP"
6305
+ },
6306
+ {
6307
+ id: "projects",
6308
+ label: "项目 MCP"
6309
+ },
6310
+ {
6311
+ id: "market",
6312
+ label: "MCP 市场"
6313
+ }
6314
+ ];
6315
+ /**
6316
+ * MCP 管理面板:顶层「MCP 管理」Tab 下的三个子页签容器。
6317
+ * 作者 ddj 2026年08月27号
6318
+ * @param {object[]} servers 全局 MCP 服务列表
6319
+ * @param {object[]} projects 项目及其项目级 MCP 列表
6320
+ * @param {string} busy 正在执行的操作标识
6321
+ * @param {object} draft 新增表单草稿
6322
+ * @param {Function} edit 草稿字段变更回调
6323
+ * @param {Function} resetDraft 清空草稿回调
6324
+ */
6325
+ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject, saveGlobal, saveProject }) {
6326
+ const [mcpTab, setMcpTab] = react.default.useState("mine");
6327
+ const [showForm, setShowForm] = react.default.useState(false);
6328
+ const [projectForm, setProjectForm] = react.default.useState(null);
6329
+ const [selectedPath, setSelectedPath] = react.default.useState("");
6330
+ react.default.useEffect(() => {
6331
+ if (!projects.length) {
6332
+ setSelectedPath("");
6333
+ return;
6334
+ }
6335
+ if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath);
6336
+ }, [projects, selectedPath]);
6337
+ const closeGlobalForm = () => setShowForm(false);
6338
+ const closeProjectForm = () => setProjectForm(null);
6339
+ const addProject = (project) => {
6340
+ resetDraft?.();
6341
+ setProjectForm({
6342
+ workspacePath: project.workspacePath,
6343
+ title: project.title
6344
+ });
6345
+ };
6346
+ const selectedProject = projects.find((project) => project.workspacePath === selectedPath);
6347
+ const mineBody = servers.length ? servers.map((s) => react.default.createElement(ServerCard, {
6348
+ key: s.id,
6349
+ server: s,
6350
+ onRefresh: refreshGlobal,
6351
+ onToggle: toggleGlobal,
6352
+ onRemove: removeGlobal
6353
+ })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有配置全局 MCP");
6354
+ const projectBody = projects.length ? react.default.createElement(react.default.Fragment, null, react.default.createElement(ProjectPicker, {
6355
+ projects,
6356
+ value: selectedPath,
6357
+ onChange: setSelectedPath
6358
+ }), selectedProject ? react.default.createElement(ProjectGroup, {
6359
+ project: selectedProject,
6360
+ busy,
6361
+ onAdd: addProject,
6362
+ onRefresh: refreshProject,
6363
+ onToggle: toggleProject,
6364
+ onRemove: removeProject
6365
+ }) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "请选择项目")) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有项目");
6366
+ let body;
6367
+ if (mcpTab === "projects") body = projectBody;
6368
+ else if (mcpTab === "market") body = react.default.createElement("div", { className: "vsm-mcp-empty" }, "MCP 市场暂未接入");
6369
+ else body = mineBody;
6370
+ const form = showForm ? react.default.createElement(McpForm, {
6371
+ title: "添加全局 MCP",
6372
+ draft,
6373
+ busy,
6374
+ edit,
6375
+ save: () => saveGlobal(closeGlobalForm),
6376
+ close: closeGlobalForm
6377
+ }) : projectForm ? react.default.createElement(McpForm, {
6378
+ title: "添加项目 MCP · " + projectForm.title,
6379
+ draft,
6380
+ busy,
6381
+ edit,
6382
+ save: () => saveProject(projectForm.workspacePath, closeProjectForm),
6383
+ close: closeProjectForm
6384
+ }) : null;
6385
+ return react.default.createElement(react.default.Fragment, null, react.default.createElement("div", { className: "vsm-mcp-subhead" }, react.default.createElement("nav", { className: "vsm-mcp-subtabs" }, MCP_TABS.map((item) => react.default.createElement("button", {
6386
+ key: item.id,
6387
+ className: mcpTab === item.id ? "active" : "",
6388
+ onClick: () => setMcpTab(item.id)
6389
+ }, item.label))), react.default.createElement("button", {
6390
+ className: "vsm-primary vsm-small",
6391
+ onClick: () => {
6392
+ resetDraft?.();
6393
+ setShowForm(true);
6394
+ }
6395
+ }, "+ 添加全局 MCP")), body, form);
5396
6396
  }
5397
6397
  /** MCP 管理主面板。 */
5398
6398
  function McpSettings(props) {
@@ -5405,16 +6405,6 @@ window.__ModuleLoader__.load({
5405
6405
  const [error, setError] = react.default.useState("");
5406
6406
  const [tab, setTab] = react.default.useState("general");
5407
6407
  const [draft, setDraft] = react.default.useState(EMPTY);
5408
- const [showForm, setShowForm] = react.default.useState(false);
5409
- const [projectForm, setProjectForm] = react.default.useState(null);
5410
- const [selectedPath, setSelectedPath] = react.default.useState("");
5411
- react.default.useEffect(() => {
5412
- if (!projects.length) {
5413
- setSelectedPath("");
5414
- return;
5415
- }
5416
- if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath);
5417
- }, [projects, selectedPath]);
5418
6408
  const loadAll = react.default.useCallback(() => {
5419
6409
  setLoading(true);
5420
6410
  setError("");
@@ -5464,116 +6454,87 @@ window.__ModuleLoader__.load({
5464
6454
  workspacePath: p.workspacePath,
5465
6455
  serverName: s.serverName
5466
6456
  }, replaceProject(p.workspacePath));
5467
- const saveGlobal = () => {
6457
+ /**
6458
+ * 保存全局 MCP:成功后由回调关闭子面板。
6459
+ * 作者 ddj 2026年08月27号
6460
+ * @param {Function} close 关闭新增表单回调
6461
+ */
6462
+ const saveGlobal = (close) => {
5468
6463
  setBusy("save");
5469
6464
  setError("");
5470
6465
  rpc("mcp.save", { config: configOf(draft) }).then(finish).then((result) => {
5471
6466
  setServers((old) => old.some((s) => s.id === result.server.id) ? old.map((s) => s.id === result.server.id ? result.server : s) : old.concat(result.server));
5472
6467
  setDraft(EMPTY);
5473
- setShowForm(false);
6468
+ close();
5474
6469
  }).catch((e) => setError(String(e))).finally(() => setBusy(""));
5475
6470
  };
5476
- const saveProject = () => {
5477
- if (!projectForm) return;
6471
+ /**
6472
+ * 保存项目 MCP:成功后由回调关闭子面板。
6473
+ * 作者 ddj 2026年08月27号
6474
+ * @param {string} workspacePath 目标项目路径
6475
+ * @param {Function} close 关闭新增表单回调
6476
+ */
6477
+ const saveProject = (workspacePath, close) => {
5478
6478
  setBusy("project-save");
5479
6479
  setError("");
5480
6480
  const config = configOf(draft);
5481
6481
  rpc("mcp.projectSave", {
5482
- workspacePath: projectForm.workspacePath,
6482
+ workspacePath,
5483
6483
  serverName: config.serverName,
5484
6484
  config
5485
6485
  }).then(finish).then((result) => {
5486
- setProjects((old) => old.map((p) => p.workspacePath === projectForm.workspacePath ? result.project : p));
6486
+ setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p));
5487
6487
  setDraft(EMPTY);
5488
- setProjectForm(null);
6488
+ close();
5489
6489
  }).catch((e) => setError(String(e))).finally(() => setBusy(""));
5490
6490
  };
5491
6491
  const edit = (key, value) => setDraft((old) => ({
5492
6492
  ...old,
5493
6493
  [key]: value
5494
6494
  }));
5495
- const marketBody = react.default.createElement("div", { className: "vsm-mcp-empty" }, "MCP 市场暂未接入");
5496
- const selectedProject = projects.find((project) => project.workspacePath === selectedPath);
5497
- const projectBody = projects.length ? react.default.createElement(react.default.Fragment, null, react.default.createElement(ProjectPicker, {
5498
- projects,
5499
- value: selectedPath,
5500
- onChange: setSelectedPath
5501
- }), selectedProject ? react.default.createElement(ProjectGroup, {
5502
- project: selectedProject,
5503
- busy,
5504
- onAdd: (project) => {
5505
- setDraft(EMPTY);
5506
- setProjectForm({
5507
- workspacePath: project.workspacePath,
5508
- title: project.title
5509
- });
5510
- },
5511
- onRefresh: refreshProject,
5512
- onToggle: toggleProject,
5513
- onRemove: removeProject
5514
- }) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "请选择项目")) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有项目");
6495
+ const resetDraft = () => setDraft(EMPTY);
5515
6496
  let body;
5516
6497
  if (loading) body = react.default.createElement("div", { className: "vsm-mcp-empty" }, "正在读取 MCP 服务…");
5517
6498
  else if (tab === "general") body = react.default.createElement(GeneralSettings, { registry: openerRegistry });
5518
6499
  else if (tab === "keys") body = react.default.createElement(KeybindingsPanel, null);
5519
- else if (tab === "mine") body = react.default.createElement("div", null, servers.length ? servers.map((s) => react.default.createElement(ServerCard, {
5520
- key: s.id,
5521
- server: s,
5522
- onRefresh: refreshGlobal,
5523
- onToggle: toggleGlobal,
5524
- onRemove: removeGlobal
5525
- })) : react.default.createElement("div", { className: "vsm-mcp-empty" }, "还没有配置全局 MCP"));
5526
- else if (tab === "projects") body = projectBody;
5527
- else if (tab === "compat") body = react.default.createElement(CompatSection, { getSummary: compatSummary });
5528
- else if (tab === "lsp") body = react.default.createElement(LspSettings, null);
5529
- else if (tab === "perf") body = react.default.createElement(PerfSettings, null);
5530
- else body = marketBody;
5531
- const form = showForm ? react.default.createElement(McpForm, {
5532
- title: "添加全局 MCP",
5533
- draft,
6500
+ else if (tab === "mcp") body = react.default.createElement(McpManagePanel, {
6501
+ servers,
6502
+ projects,
5534
6503
  busy,
5535
- edit,
5536
- save: saveGlobal,
5537
- close: () => setShowForm(false)
5538
- }) : projectForm ? react.default.createElement(McpForm, {
5539
- title: "添加项目 MCP · " + projectForm.title,
5540
6504
  draft,
5541
- busy,
5542
6505
  edit,
5543
- save: saveProject,
5544
- close: () => setProjectForm(null)
5545
- }) : null;
5546
- return react.default.createElement("section", { className: "vsm-mcp-page" }, react.default.createElement("header", { className: "vsm-mcp-header" }, react.default.createElement("div", null, react.default.createElement("h2", null, "VSCodeMode"), react.default.createElement("p", null, "管理当前 profile 与各项目的 Model Context Protocol 服务。")), react.default.createElement("button", {
5547
- className: "vsm-primary",
5548
- onClick: () => {
5549
- setDraft(EMPTY);
5550
- setShowForm(true);
5551
- }
5552
- }, "+ 添加全局 MCP")), react.default.createElement("nav", { className: "vsm-mcp-tabs" }, react.default.createElement("button", {
6506
+ resetDraft,
6507
+ saveGlobal: (close) => saveGlobal(close),
6508
+ saveProject: (workspacePath, close) => saveProject(workspacePath, close),
6509
+ refreshGlobal,
6510
+ toggleGlobal,
6511
+ removeGlobal,
6512
+ refreshProject,
6513
+ toggleProject,
6514
+ removeProject
6515
+ });
6516
+ else if (tab === "compat") body = react.default.createElement(CompatSection, { getSummary: compatSummary });
6517
+ else if (tab === "lsp") body = react.default.createElement(LspSettings, null);
6518
+ else body = react.default.createElement(PerfSettings, null);
6519
+ return react.default.createElement("section", { className: "vsm-mcp-page" }, react.default.createElement("header", { className: "vsm-mcp-header" }, react.default.createElement("div", null, react.default.createElement("h2", null, "VSCodeMode"), react.default.createElement("p", null, "管理当前 profile 与各项目的 Model Context Protocol 服务。"))), react.default.createElement("nav", { className: "vsm-mcp-tabs" }, react.default.createElement("button", {
5553
6520
  className: tab === "general" ? "active" : "",
5554
6521
  onClick: () => setTab("general")
5555
6522
  }, "通用"), react.default.createElement("button", {
5556
6523
  className: tab === "keys" ? "active" : "",
5557
6524
  onClick: () => setTab("keys")
5558
6525
  }, "快捷键"), react.default.createElement("button", {
5559
- className: tab === "mine" ? "active" : "",
5560
- onClick: () => setTab("mine")
5561
- }, "我的 MCP"), react.default.createElement("button", {
5562
- className: tab === "projects" ? "active" : "",
5563
- onClick: () => setTab("projects")
5564
- }, "项目 MCP"), react.default.createElement("button", {
6526
+ className: tab === "mcp" ? "active" : "",
6527
+ onClick: () => setTab("mcp")
6528
+ }, "MCP 管理"), react.default.createElement("button", {
5565
6529
  className: tab === "lsp" ? "active" : "",
5566
6530
  onClick: () => setTab("lsp")
5567
6531
  }, "语言服务器"), react.default.createElement("button", {
5568
6532
  className: tab === "perf" ? "active" : "",
5569
6533
  onClick: () => setTab("perf")
5570
6534
  }, "性能优化"), react.default.createElement("button", {
5571
- className: tab === "market" ? "active" : "",
5572
- onClick: () => setTab("market")
5573
- }, "MCP 市场"), react.default.createElement("button", {
5574
6535
  className: tab === "compat" ? "active" : "",
5575
6536
  onClick: () => setTab("compat")
5576
- }, "兼容性")), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), body, form);
6537
+ }, "兼容性")), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error), body);
5577
6538
  }
5578
6539
  /** MCP 新增表单(全局/项目共用)。 */
5579
6540
  function McpForm({ title, draft, busy, edit, save, close }) {