@daweifu/capability-menu 0.1.3 → 0.1.5

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
@@ -3,9 +3,97 @@ var module = { exports: {} };
3
3
  var exports = module.exports;
4
4
 
5
5
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ //#region \0rolldown/runtime.js
7
+ var __create = Object.create;
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
+ key = keys[i];
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+ //#endregion
6
28
  let react = require("react");
7
29
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
30
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
8
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
+ //#region src/client/settings-nav-icon.ts
33
+ /** Replace the settings shell's fallback gear with dsh's shared list glyph. */
34
+ const NAV_ICON_MARKER = "data-capability-menu-nav-icon";
35
+ const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button";
36
+ const NAV_ICON_SIZE = 16;
37
+ /** The geometry of dsh-client-ui-primitives' IconFlatListOutlineMedium. */
38
+ function flatListMaskUrl() {
39
+ return `data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#000\" stroke-linecap=\"round\" stroke-width=\"1.3\"><path d=\"M6 3.5h7.5M6 8h7.5M6 12.5h7.5\"/><path d=\"M2.6 3.5h.01M2.6 8h.01M2.6 12.5h.01\"/></svg>")}`;
40
+ }
41
+ function navIconCss(maskUrl) {
42
+ return [
43
+ `[${NAV_ICON_MARKER}] > svg { display: none; }`,
44
+ `[${NAV_ICON_MARKER}]::before {`,
45
+ " content: \"\";",
46
+ " flex: none;",
47
+ ` width: ${NAV_ICON_SIZE}px;`,
48
+ ` height: ${NAV_ICON_SIZE}px;`,
49
+ " background-color: currentColor;",
50
+ ` -webkit-mask: url("${maskUrl}") center / ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px no-repeat;`,
51
+ ` mask: url("${maskUrl}") center / ${NAV_ICON_SIZE}px ${NAV_ICON_SIZE}px no-repeat;`,
52
+ "}"
53
+ ].join("\n");
54
+ }
55
+ /**
56
+ * The current settings.section slot has no icon option. Match this plugin's
57
+ * localized nav label and replace only its fallback gear, as dsh-market does.
58
+ */
59
+ function installSettingsNavIcon(ctx, resolveLabel) {
60
+ if (typeof document === "undefined") return;
61
+ ctx.effect(() => {
62
+ const style = document.createElement("style");
63
+ style.dataset.plugin = "capability-menu";
64
+ style.dataset.pluginCss = "capability-menu/settings-nav-icon";
65
+ style.textContent = navIconCss(flatListMaskUrl());
66
+ document.head.appendChild(style);
67
+ let disposed = false;
68
+ let scheduled = false;
69
+ const sync = () => {
70
+ scheduled = false;
71
+ if (disposed) return;
72
+ const wanted = resolveLabel().trim();
73
+ for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) if (wanted.length > 0 && row.textContent?.trim() === wanted) row.setAttribute(NAV_ICON_MARKER, "");
74
+ else row.removeAttribute(NAV_ICON_MARKER);
75
+ };
76
+ const schedule = () => {
77
+ if (scheduled || disposed) return;
78
+ scheduled = true;
79
+ queueMicrotask(sync);
80
+ };
81
+ sync();
82
+ const observer = new MutationObserver(schedule);
83
+ observer.observe(document.body, {
84
+ childList: true,
85
+ subtree: true,
86
+ characterData: true
87
+ });
88
+ return () => {
89
+ disposed = true;
90
+ observer.disconnect();
91
+ for (const row of document.querySelectorAll(`[${NAV_ICON_MARKER}]`)) row.removeAttribute(NAV_ICON_MARKER);
92
+ style.remove();
93
+ };
94
+ }, "capability-menu: settings nav icon");
95
+ }
96
+ //#endregion
9
97
  //#region node_modules/.pnpm/zod@4.5.2/node_modules/zod/v4/core/util.js
10
98
  function getEnumValues(entries) {
11
99
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5190,6 +5278,18 @@ function superRefine(fn, params) {
5190
5278
  * namespace and exports a `TYPERT_REMOTE` contribution that the browser
5191
5279
  * plugin mounts via `ctx.remote.$mount(...)`.
5192
5280
  */
5281
+ /**
5282
+ * Bridge Typert's codec field rename across DSH 0.1 and 0.2. Older remotes
5283
+ * read `schema`; the 0.2 protocol lazily calls `create()`.
5284
+ */
5285
+ function strictCodec(typeSymbol, schema) {
5286
+ return {
5287
+ mode: "strict",
5288
+ typeSymbol,
5289
+ schema,
5290
+ create: () => schema
5291
+ };
5292
+ }
5193
5293
  const stringRecord$schema = record(string(), string());
5194
5294
  const mcpLocation$schema = object({
5195
5295
  id: string().readonly(),
@@ -5240,6 +5340,7 @@ const capabilityRow$schema = object({
5240
5340
  name: string().readonly(),
5241
5341
  server: string().optional().readonly(),
5242
5342
  source: string().optional().readonly(),
5343
+ preset: string().optional().readonly(),
5243
5344
  class: union([
5244
5345
  literal("resident"),
5245
5346
  literal("on-demand"),
@@ -5273,7 +5374,8 @@ const toolDetail$schema = object({
5273
5374
  provider: string().readonly(),
5274
5375
  serverName: string().optional().readonly(),
5275
5376
  path: string().optional().readonly(),
5276
- source: string().optional().readonly()
5377
+ source: string().optional().readonly(),
5378
+ preset: string().optional().readonly()
5277
5379
  }).readonly(),
5278
5380
  tags: array(string()).readonly(),
5279
5381
  stats: object({
@@ -5294,11 +5396,7 @@ const TYPERT_REMOTE = {
5294
5396
  method: "getConfig",
5295
5397
  invocation: { kind: "direct" },
5296
5398
  parameters: [],
5297
- result: {
5298
- mode: "strict",
5299
- typeSymbol: "Record<string, unknown>",
5300
- schema: record(string(), unknown())
5301
- },
5399
+ result: strictCodec("Record<string, unknown>", record(string(), unknown())),
5302
5400
  sourceLocation: {
5303
5401
  file: "src/server/remote.ts",
5304
5402
  line: 61,
@@ -5315,17 +5413,9 @@ const TYPERT_REMOTE = {
5315
5413
  name: "partial",
5316
5414
  wire: "partial",
5317
5415
  source: "json",
5318
- codec: {
5319
- mode: "strict",
5320
- typeSymbol: "Record<string, unknown>",
5321
- schema: record(string(), unknown())
5322
- }
5416
+ codec: strictCodec("Record<string, unknown>", record(string(), unknown()))
5323
5417
  }],
5324
- result: {
5325
- mode: "strict",
5326
- typeSymbol: "void",
5327
- schema: _undefined()
5328
- },
5418
+ result: strictCodec("void", _undefined()),
5329
5419
  sourceLocation: {
5330
5420
  file: "src/server/remote.ts",
5331
5421
  line: 67,
@@ -5339,11 +5429,7 @@ const TYPERT_REMOTE = {
5339
5429
  method: "classifyAll",
5340
5430
  invocation: { kind: "direct" },
5341
5431
  parameters: [],
5342
- result: {
5343
- mode: "strict",
5344
- typeSymbol: "@daweifu/capability-menu#CapabilityRow",
5345
- schema: array(capabilityRow$schema)
5346
- },
5432
+ result: strictCodec("@daweifu/capability-menu#CapabilityRow", array(capabilityRow$schema)),
5347
5433
  sourceLocation: {
5348
5434
  file: "src/server/remote.ts",
5349
5435
  line: 73,
@@ -5357,11 +5443,7 @@ const TYPERT_REMOTE = {
5357
5443
  method: "refresh",
5358
5444
  invocation: { kind: "direct" },
5359
5445
  parameters: [],
5360
- result: {
5361
- mode: "strict",
5362
- typeSymbol: "void",
5363
- schema: _undefined()
5364
- },
5446
+ result: strictCodec("void", _undefined()),
5365
5447
  sourceLocation: {
5366
5448
  file: "src/server/remote.ts",
5367
5449
  line: 84,
@@ -5378,27 +5460,15 @@ const TYPERT_REMOTE = {
5378
5460
  name: "id",
5379
5461
  wire: "id",
5380
5462
  source: "json",
5381
- codec: {
5382
- mode: "strict",
5383
- typeSymbol: "string",
5384
- schema: string()
5385
- }
5463
+ codec: strictCodec("string", string())
5386
5464
  }, {
5387
5465
  name: "relPath",
5388
5466
  wire: "relPath",
5389
5467
  source: "json",
5390
5468
  acceptsUndefined: true,
5391
- codec: {
5392
- mode: "strict",
5393
- typeSymbol: "string",
5394
- schema: string().optional()
5395
- }
5469
+ codec: strictCodec("string", string().optional())
5396
5470
  }],
5397
- result: {
5398
- mode: "strict",
5399
- typeSymbol: "@daweifu/capability-menu#SkillFileEntry[]",
5400
- schema: array(skillFileEntry$schema).optional()
5401
- },
5471
+ result: strictCodec("@daweifu/capability-menu#SkillFileEntry[]", array(skillFileEntry$schema).optional()),
5402
5472
  sourceLocation: {
5403
5473
  file: "src/server/remote.ts",
5404
5474
  line: 85,
@@ -5415,26 +5485,14 @@ const TYPERT_REMOTE = {
5415
5485
  name: "id",
5416
5486
  wire: "id",
5417
5487
  source: "json",
5418
- codec: {
5419
- mode: "strict",
5420
- typeSymbol: "string",
5421
- schema: string()
5422
- }
5488
+ codec: strictCodec("string", string())
5423
5489
  }, {
5424
5490
  name: "relPath",
5425
5491
  wire: "relPath",
5426
5492
  source: "json",
5427
- codec: {
5428
- mode: "strict",
5429
- typeSymbol: "string",
5430
- schema: string()
5431
- }
5493
+ codec: strictCodec("string", string())
5432
5494
  }],
5433
- result: {
5434
- mode: "strict",
5435
- typeSymbol: "string",
5436
- schema: string().optional()
5437
- },
5495
+ result: strictCodec("string", string().optional()),
5438
5496
  sourceLocation: {
5439
5497
  file: "src/server/remote.ts",
5440
5498
  line: 91,
@@ -5451,17 +5509,9 @@ const TYPERT_REMOTE = {
5451
5509
  name: "id",
5452
5510
  wire: "id",
5453
5511
  source: "json",
5454
- codec: {
5455
- mode: "strict",
5456
- typeSymbol: "string",
5457
- schema: string()
5458
- }
5512
+ codec: strictCodec("string", string())
5459
5513
  }],
5460
- result: {
5461
- mode: "strict",
5462
- typeSymbol: "@daweifu/capability-menu#ToolDetail",
5463
- schema: toolDetail$schema.optional()
5464
- },
5514
+ result: strictCodec("@daweifu/capability-menu#ToolDetail", toolDetail$schema.optional()),
5465
5515
  sourceLocation: {
5466
5516
  file: "src/server/remote.ts",
5467
5517
  line: 79,
@@ -5475,11 +5525,7 @@ const TYPERT_REMOTE = {
5475
5525
  method: "getCatalogDocs",
5476
5526
  invocation: { kind: "direct" },
5477
5527
  parameters: [],
5478
- result: {
5479
- mode: "strict",
5480
- typeSymbol: "@daweifu/capability-menu#CatalogDocs",
5481
- schema: catalogDocs$schema
5482
- },
5528
+ result: strictCodec("@daweifu/capability-menu#CatalogDocs", catalogDocs$schema),
5483
5529
  sourceLocation: {
5484
5530
  file: "src/server/remote.ts",
5485
5531
  line: 104,
@@ -5493,11 +5539,7 @@ const TYPERT_REMOTE = {
5493
5539
  method: "listLocations",
5494
5540
  invocation: { kind: "direct" },
5495
5541
  parameters: [],
5496
- result: {
5497
- mode: "strict",
5498
- typeSymbol: "@daweifu/capability-menu#McpLocation[]",
5499
- schema: array(mcpLocation$schema)
5500
- },
5542
+ result: strictCodec("@daweifu/capability-menu#McpLocation[]", array(mcpLocation$schema)),
5501
5543
  sourceLocation: {
5502
5544
  file: "src/server/remote.ts",
5503
5545
  line: 193,
@@ -5514,17 +5556,9 @@ const TYPERT_REMOTE = {
5514
5556
  name: "input",
5515
5557
  wire: "input",
5516
5558
  source: "json",
5517
- codec: {
5518
- mode: "strict",
5519
- typeSymbol: "@daweifu/capability-menu#McpInput",
5520
- schema: mcpInput$schema
5521
- }
5559
+ codec: strictCodec("@daweifu/capability-menu#McpInput", mcpInput$schema)
5522
5560
  }],
5523
- result: {
5524
- mode: "strict",
5525
- typeSymbol: "string",
5526
- schema: string()
5527
- },
5561
+ result: strictCodec("string", string()),
5528
5562
  sourceLocation: {
5529
5563
  file: "src/server/remote.ts",
5530
5564
  line: 199,
@@ -5541,17 +5575,9 @@ const TYPERT_REMOTE = {
5541
5575
  name: "id",
5542
5576
  wire: "id",
5543
5577
  source: "json",
5544
- codec: {
5545
- mode: "strict",
5546
- typeSymbol: "string",
5547
- schema: string()
5548
- }
5578
+ codec: strictCodec("string", string())
5549
5579
  }],
5550
- result: {
5551
- mode: "strict",
5552
- typeSymbol: "boolean",
5553
- schema: boolean()
5554
- },
5580
+ result: strictCodec("boolean", boolean()),
5555
5581
  sourceLocation: {
5556
5582
  file: "src/server/remote.ts",
5557
5583
  line: 205,
@@ -5568,26 +5594,14 @@ const TYPERT_REMOTE = {
5568
5594
  name: "id",
5569
5595
  wire: "id",
5570
5596
  source: "json",
5571
- codec: {
5572
- mode: "strict",
5573
- typeSymbol: "string",
5574
- schema: string()
5575
- }
5597
+ codec: strictCodec("string", string())
5576
5598
  }, {
5577
5599
  name: "input",
5578
5600
  wire: "input",
5579
5601
  source: "json",
5580
- codec: {
5581
- mode: "strict",
5582
- typeSymbol: "@daweifu/capability-menu#McpUpdateInput",
5583
- schema: mcpUpdateInput$schema
5584
- }
5602
+ codec: strictCodec("@daweifu/capability-menu#McpUpdateInput", mcpUpdateInput$schema)
5585
5603
  }],
5586
- result: {
5587
- mode: "strict",
5588
- typeSymbol: "boolean",
5589
- schema: boolean()
5590
- },
5604
+ result: strictCodec("boolean", boolean()),
5591
5605
  sourceLocation: {
5592
5606
  file: "src/server/remote.ts",
5593
5607
  line: 211,
@@ -5601,11 +5615,7 @@ const TYPERT_REMOTE = {
5601
5615
  method: "listSkillLocations",
5602
5616
  invocation: { kind: "direct" },
5603
5617
  parameters: [],
5604
- result: {
5605
- mode: "strict",
5606
- typeSymbol: "@daweifu/capability-menu#SkillLocation[]",
5607
- schema: array(skillLocation$schema)
5608
- },
5618
+ result: strictCodec("@daweifu/capability-menu#SkillLocation[]", array(skillLocation$schema)),
5609
5619
  sourceLocation: {
5610
5620
  file: "src/server/remote.ts",
5611
5621
  line: 229,
@@ -5622,33 +5632,46 @@ const TYPERT_REMOTE = {
5622
5632
  name: "dir",
5623
5633
  wire: "dir",
5624
5634
  source: "json",
5625
- codec: {
5626
- mode: "strict",
5627
- typeSymbol: "string",
5628
- schema: string()
5629
- }
5635
+ codec: strictCodec("string", string())
5630
5636
  }, {
5631
5637
  name: "projectPath",
5632
5638
  wire: "projectPath",
5633
5639
  source: "json",
5634
5640
  acceptsUndefined: true,
5635
- codec: {
5636
- mode: "strict",
5637
- typeSymbol: "string",
5638
- schema: string().optional()
5639
- }
5641
+ codec: strictCodec("string", string().optional())
5640
5642
  }],
5641
- result: {
5642
- mode: "strict",
5643
- typeSymbol: "string",
5644
- schema: string()
5645
- },
5643
+ result: strictCodec("string", string()),
5646
5644
  sourceLocation: {
5647
5645
  file: "src/server/remote.ts",
5648
5646
  line: 252,
5649
5647
  column: 3
5650
5648
  }
5651
5649
  },
5650
+ {
5651
+ id: "@daweifu/capability-menu#capabilityPolicy/importSkillFromGitHub",
5652
+ service: "capabilityPolicy",
5653
+ namespace: "capabilityPolicy",
5654
+ method: "importSkillFromGitHub",
5655
+ invocation: { kind: "direct" },
5656
+ parameters: [{
5657
+ name: "url",
5658
+ wire: "url",
5659
+ source: "json",
5660
+ codec: strictCodec("string", string())
5661
+ }, {
5662
+ name: "projectPath",
5663
+ wire: "projectPath",
5664
+ source: "json",
5665
+ acceptsUndefined: true,
5666
+ codec: strictCodec("string", string().optional())
5667
+ }],
5668
+ result: strictCodec("string", string()),
5669
+ sourceLocation: {
5670
+ file: "src/server/remote.ts",
5671
+ line: 263,
5672
+ column: 3
5673
+ }
5674
+ },
5652
5675
  {
5653
5676
  id: "@daweifu/capability-menu#capabilityPolicy/removeSkillLocation",
5654
5677
  service: "capabilityPolicy",
@@ -5659,27 +5682,15 @@ const TYPERT_REMOTE = {
5659
5682
  name: "name",
5660
5683
  wire: "name",
5661
5684
  source: "json",
5662
- codec: {
5663
- mode: "strict",
5664
- typeSymbol: "string",
5665
- schema: string()
5666
- }
5685
+ codec: strictCodec("string", string())
5667
5686
  }, {
5668
5687
  name: "entryDir",
5669
5688
  wire: "entryDir",
5670
5689
  source: "json",
5671
5690
  acceptsUndefined: true,
5672
- codec: {
5673
- mode: "strict",
5674
- typeSymbol: "string",
5675
- schema: string().optional()
5676
- }
5691
+ codec: strictCodec("string", string().optional())
5677
5692
  }],
5678
- result: {
5679
- mode: "strict",
5680
- typeSymbol: "boolean",
5681
- schema: boolean()
5682
- },
5693
+ result: strictCodec("boolean", boolean()),
5683
5694
  sourceLocation: {
5684
5695
  file: "src/server/remote.ts",
5685
5696
  line: 258,
@@ -5696,17 +5707,9 @@ const TYPERT_REMOTE = {
5696
5707
  name: "name",
5697
5708
  wire: "name",
5698
5709
  source: "json",
5699
- codec: {
5700
- mode: "strict",
5701
- typeSymbol: "string",
5702
- schema: string()
5703
- }
5710
+ codec: strictCodec("string", string())
5704
5711
  }],
5705
- result: {
5706
- mode: "strict",
5707
- typeSymbol: "string",
5708
- schema: string()
5709
- },
5712
+ result: strictCodec("string", string()),
5710
5713
  sourceLocation: {
5711
5714
  file: "src/server/remote.ts",
5712
5715
  line: 259,
@@ -5724,44 +5727,42 @@ const TYPERT_REMOTE = {
5724
5727
  name: "name",
5725
5728
  wire: "name",
5726
5729
  source: "json",
5727
- codec: {
5728
- mode: "strict",
5729
- typeSymbol: "string",
5730
- schema: string()
5731
- }
5730
+ codec: strictCodec("string", string())
5732
5731
  },
5733
5732
  {
5734
5733
  name: "dir",
5735
5734
  wire: "dir",
5736
5735
  source: "json",
5737
- codec: {
5738
- mode: "strict",
5739
- typeSymbol: "string",
5740
- schema: string()
5741
- }
5736
+ codec: strictCodec("string", string())
5742
5737
  },
5743
5738
  {
5744
5739
  name: "entryDir",
5745
5740
  wire: "entryDir",
5746
5741
  source: "json",
5747
5742
  acceptsUndefined: true,
5748
- codec: {
5749
- mode: "strict",
5750
- typeSymbol: "string",
5751
- schema: string().optional()
5752
- }
5743
+ codec: strictCodec("string", string().optional())
5753
5744
  }
5754
5745
  ],
5755
- result: {
5756
- mode: "strict",
5757
- typeSymbol: "boolean",
5758
- schema: boolean()
5759
- },
5746
+ result: strictCodec("boolean", boolean()),
5760
5747
  sourceLocation: {
5761
5748
  file: "src/server/remote.ts",
5762
5749
  line: 264,
5763
5750
  column: 3
5764
5751
  }
5752
+ },
5753
+ {
5754
+ id: "@daweifu/capability-menu#capabilityPolicy/catalogVersion",
5755
+ service: "capabilityPolicy",
5756
+ namespace: "capabilityPolicy",
5757
+ method: "catalogVersion",
5758
+ invocation: { kind: "direct" },
5759
+ parameters: [],
5760
+ result: strictCodec("number", number()),
5761
+ sourceLocation: {
5762
+ file: "src/server/remote.ts",
5763
+ line: 317,
5764
+ column: 3
5765
+ }
5765
5766
  }
5766
5767
  ]
5767
5768
  };
@@ -5796,15 +5797,52 @@ function unwrapMessage(result) {
5796
5797
  * still followed by an authoritative one.
5797
5798
  */
5798
5799
  let lastSnapshot;
5800
+ /** Short grace period for host tools that register just after the section mounts. */
5801
+ const EMPTY_SNAPSHOT_RETRY_DELAYS = [
5802
+ 250,
5803
+ 500,
5804
+ 1e3,
5805
+ 1500
5806
+ ];
5799
5807
  /** The previously loaded snapshot, when this page session has one. */
5800
5808
  function cachedSnapshot() {
5801
5809
  return lastSnapshot;
5802
5810
  }
5803
- /** Load the classification list from the remote, remembering it for remounts. */
5804
- async function loadSnapshot(remote) {
5805
- lastSnapshot = { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
5811
+ /** Read one authoritative classification list without changing the view cache. */
5812
+ async function readSnapshot(remote) {
5813
+ return { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
5814
+ }
5815
+ /** Remember a snapshot after its contents are ready to replace the current view. */
5816
+ function rememberSnapshot(snapshot) {
5817
+ lastSnapshot = snapshot;
5806
5818
  return lastSnapshot;
5807
5819
  }
5820
+ /**
5821
+ * Load the classification list, retrying an empty result briefly because
5822
+ * built-in tools can register just after the settings section mounts.
5823
+ * Non-empty results remain authoritative and return immediately.
5824
+ */
5825
+ async function loadSnapshot(remote, retryDelays = EMPTY_SNAPSHOT_RETRY_DELAYS) {
5826
+ let snapshot = await readSnapshot(remote);
5827
+ for (const delayMs of retryDelays) {
5828
+ if (snapshot.rows.length > 0) break;
5829
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
5830
+ snapshot = await readSnapshot(remote);
5831
+ }
5832
+ return rememberSnapshot(snapshot);
5833
+ }
5834
+ /** Wait briefly for an MCP's tools to enter the host catalog after registration. */
5835
+ async function waitForMcpTools(remote, serverName, retryDelays) {
5836
+ const prefix = `mcp__${serverName}__`;
5837
+ const hasServerTools = (snapshot) => snapshot.rows.some((row) => row.kind === "tool" && row.id.startsWith(prefix));
5838
+ let snapshot = await readSnapshot(remote);
5839
+ if (hasServerTools(snapshot)) return snapshot;
5840
+ for (const delayMs of retryDelays) {
5841
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
5842
+ snapshot = await readSnapshot(remote);
5843
+ if (hasServerTools(snapshot)) return snapshot;
5844
+ }
5845
+ }
5808
5846
  //#endregion
5809
5847
  //#region src/client/LocationModal.tsx
5810
5848
  /**
@@ -5850,6 +5888,11 @@ textarea.lm-input{resize:vertical;font-family:var(--dsw-font-markdown-code-block
5850
5888
  annotates and takes the caption colour so it does not read as another label. */
5851
5889
  .lm-hint{grid-column:2;margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}
5852
5890
  .lm-error{margin:0;font-size:12px;color:#b3261e;word-break:break-word}
5891
+ .lm-progress{display:flex;align-items:center;gap:8px;margin:0;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}
5892
+ .lm-spinner{width:12px;height:12px;flex:none;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:lm-spin .8s linear infinite}
5893
+ @keyframes lm-spin{to{transform:rotate(360deg)}}
5894
+ @media(prefers-reduced-motion:reduce){.lm-spinner{animation-duration:2s}}
5895
+ .mc-tab:disabled,.mc-preview-close:disabled{cursor:default;opacity:.6}
5853
5896
  .lm-actions{display:flex;gap:8px;justify-content:flex-end;padding:4px 0 0}
5854
5897
  /* Removal is irreversible, so it takes over the action row for one confirmation
5855
5898
  step: the message states the consequence, then the buttons act. */
@@ -5889,8 +5932,9 @@ function LocationModal(props) {
5889
5932
  const { remote, t, defaultKind = "mcp", editMcp, editSkill, onClose, onChanged } = props;
5890
5933
  const editing = editMcp !== void 0 || editSkill !== void 0;
5891
5934
  const [subTab, setSubTab] = (0, react.useState)(editSkill !== void 0 ? "skill" : editMcp !== void 0 ? "mcp" : defaultKind);
5892
- const [error, setError] = (0, react.useState)(null);
5935
+ const [errors, setErrors] = (0, react.useState)({});
5893
5936
  const [busy, setBusy] = (0, react.useState)(false);
5937
+ const [progressMessage, setProgressMessage] = (0, react.useState)(null);
5894
5938
  /**
5895
5939
  * Which action is waiting for confirmation, if any. Two need one: 移除, and
5896
5940
  * 保存 on an entry that is a real directory — repointing unlinks the entry
@@ -5928,31 +5972,49 @@ function LocationModal(props) {
5928
5972
  t
5929
5973
  ]);
5930
5974
  /** Run a mutation; on success close and ask the parent to re-pull. */
5931
- const submit = (0, react.useCallback)(async (action, noticeFor) => {
5975
+ const submit = (0, react.useCallback)(async (action, noticeFor, progress, registeredMcp) => {
5932
5976
  if (busy) return;
5933
5977
  setBusy(true);
5978
+ setProgressMessage(progress ?? null);
5979
+ setErrors((prev) => {
5980
+ const next = { ...prev };
5981
+ delete next[subTab];
5982
+ return next;
5983
+ });
5934
5984
  try {
5935
5985
  const changed = await action();
5936
5986
  if (changed === false) throw new Error(t("entryNotFound"));
5937
- setError(null);
5938
- onChanged(noticeFor?.(changed));
5987
+ setErrors((prev) => {
5988
+ const next = { ...prev };
5989
+ delete next[subTab];
5990
+ return next;
5991
+ });
5992
+ onChanged(noticeFor?.(changed), registeredMcp);
5939
5993
  onClose();
5940
5994
  } catch (e) {
5941
- setError(String(e));
5995
+ setErrors((prev) => ({
5996
+ ...prev,
5997
+ [subTab]: String(e)
5998
+ }));
5942
5999
  } finally {
5943
6000
  setBusy(false);
6001
+ setProgressMessage(null);
5944
6002
  }
5945
6003
  }, [
5946
6004
  busy,
5947
6005
  onChanged,
5948
6006
  onClose,
6007
+ subTab,
5949
6008
  t
5950
6009
  ]);
5951
6010
  const submitMcp = (0, react.useCallback)(() => {
5952
6011
  const trimmedTimeout = timeoutSec.trim();
5953
6012
  const timeoutMs = trimmedTimeout.length === 0 ? void 0 : Math.round(Number(trimmedTimeout) * 1e3);
5954
6013
  if (timeoutMs !== void 0 && !Number.isFinite(timeoutMs)) {
5955
- setError(t("timeoutInvalid"));
6014
+ setErrors((prev) => ({
6015
+ ...prev,
6016
+ mcp: t("timeoutInvalid")
6017
+ }));
5956
6018
  return;
5957
6019
  }
5958
6020
  const parsedEnv = fromLines(env);
@@ -5973,7 +6035,7 @@ function LocationModal(props) {
5973
6035
  submit(async () => editMcp !== void 0 ? unwrapMessage(await remote.updateLocation(editMcp.id, common)) : unwrapMessage(await remote.addLocation({
5974
6036
  serverName: serverName.trim(),
5975
6037
  ...common
5976
- })));
6038
+ })), void 0, void 0, editMcp?.serverName ?? serverName.trim());
5977
6039
  }, [
5978
6040
  submit,
5979
6041
  remote,
@@ -5990,8 +6052,13 @@ function LocationModal(props) {
5990
6052
  t
5991
6053
  ]);
5992
6054
  const submitSkill = (0, react.useCallback)(() => {
6055
+ const source = skillDir.trim();
5993
6056
  const asked = skillRoot === "project" ? projectPath.trim() : void 0;
5994
- submit(async () => editSkill !== void 0 ? unwrapMessage(await remote.updateSkillLocation(editSkill.name, skillDir.trim(), editSkill.entryDir)) : unwrapMessage(await remote.addSkillLocation(skillDir.trim(), asked)), (result) => editSkill !== void 0 ? t("skillRepointed", { dir: skillDir.trim() }) : t("skillRegisteredAt", { path: String(result) }));
6057
+ submit(async () => {
6058
+ if (editSkill !== void 0) return unwrapMessage(await remote.updateSkillLocation(editSkill.name, source, editSkill.entryDir));
6059
+ if (/^https?:\/\//i.test(source)) return unwrapMessage(await remote.importSkillFromGitHub(source, asked));
6060
+ return unwrapMessage(await remote.addSkillLocation(source, asked));
6061
+ }, (result) => editSkill !== void 0 ? t("skillRepointed", { dir: source }) : t("skillRegisteredAt", { path: String(result) }), editSkill === void 0 && /^https:\/\//i.test(source) ? t("githubImporting") : void 0);
5995
6062
  }, [
5996
6063
  submit,
5997
6064
  remote,
@@ -6067,7 +6134,7 @@ function LocationModal(props) {
6067
6134
  if (needsSaveConfirm) setConfirm("save");
6068
6135
  else onSubmit();
6069
6136
  },
6070
- children: editing ? t("save") : t("register")
6137
+ children: progressMessage !== null ? t("importing") : editing ? t("save") : t("register")
6071
6138
  }), editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6072
6139
  type: "button",
6073
6140
  className: "lm-btn lm-btn--danger",
@@ -6078,7 +6145,7 @@ function LocationModal(props) {
6078
6145
  });
6079
6146
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6080
6147
  className: "mc-preview-mask",
6081
- onClick: onClose,
6148
+ onClick: busy ? void 0 : onClose,
6082
6149
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6083
6150
  className: "mc-preview",
6084
6151
  onClick: (e) => e.stopPropagation(),
@@ -6091,6 +6158,7 @@ function LocationModal(props) {
6091
6158
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6092
6159
  type: "button",
6093
6160
  className: "mc-preview-close",
6161
+ disabled: busy,
6094
6162
  onClick: onClose,
6095
6163
  children: t("previewClose")
6096
6164
  })]
@@ -6107,6 +6175,7 @@ function LocationModal(props) {
6107
6175
  className: "mc-tab",
6108
6176
  "aria-selected": subTab === "mcp",
6109
6177
  "data-active": subTab === "mcp" ? "true" : void 0,
6178
+ disabled: busy,
6110
6179
  onClick: () => setSubTab("mcp"),
6111
6180
  children: t("mcpServers")
6112
6181
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -6115,6 +6184,7 @@ function LocationModal(props) {
6115
6184
  className: "mc-tab",
6116
6185
  "aria-selected": subTab === "skill",
6117
6186
  "data-active": subTab === "skill" ? "true" : void 0,
6187
+ disabled: busy,
6118
6188
  onClick: () => setSubTab("skill"),
6119
6189
  children: t("skillDirs")
6120
6190
  })]
@@ -6122,222 +6192,234 @@ function LocationModal(props) {
6122
6192
  }),
6123
6193
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6124
6194
  className: "lm-body",
6125
- children: [error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6126
- className: "lm-error",
6127
- children: error
6128
- }), subTab === "mcp" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6129
- className: "lm-block",
6130
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6131
- className: "lm-form",
6132
- children: [
6133
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6134
- className: "lm-field",
6135
- children: [
6136
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6137
- className: "lm-label",
6138
- children: t("transport")
6139
- }),
6140
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6141
- className: "lm-input",
6142
- value: transport,
6143
- onChange: (e) => setTransport(e.target.value === "stdio" ? "stdio" : "streamable-http"),
6144
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6145
- value: "stdio",
6146
- children: t("transportStdio")
6147
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6148
- value: "streamable-http",
6149
- children: t("transportHttp")
6195
+ children: [
6196
+ errors[subTab] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6197
+ className: "lm-error",
6198
+ children: errors[subTab]
6199
+ }),
6200
+ progressMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
6201
+ className: "lm-progress",
6202
+ role: "status",
6203
+ "aria-live": "polite",
6204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6205
+ className: "lm-spinner",
6206
+ "aria-hidden": "true"
6207
+ }), progressMessage]
6208
+ }),
6209
+ subTab === "mcp" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6210
+ className: "lm-block",
6211
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6212
+ className: "lm-form",
6213
+ children: [
6214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6215
+ className: "lm-field",
6216
+ children: [
6217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6218
+ className: "lm-label",
6219
+ children: t("transport")
6220
+ }),
6221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6222
+ className: "lm-input",
6223
+ value: transport,
6224
+ onChange: (e) => setTransport(e.target.value === "stdio" ? "stdio" : "streamable-http"),
6225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6226
+ value: "stdio",
6227
+ children: t("transportStdio")
6228
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6229
+ value: "streamable-http",
6230
+ children: t("transportHttp")
6231
+ })]
6232
+ }),
6233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6234
+ className: "lm-hint",
6235
+ children: transport === "stdio" ? t("transportHintStdio") : t("transportHintHttp")
6236
+ })
6237
+ ]
6238
+ }),
6239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6240
+ className: "lm-field",
6241
+ children: [
6242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6243
+ className: "lm-label",
6244
+ children: t("serverName")
6245
+ }),
6246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6247
+ className: "lm-input",
6248
+ value: serverName,
6249
+ disabled: editMcp !== void 0,
6250
+ onChange: (e) => setServerName(e.target.value)
6251
+ }),
6252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6253
+ className: "lm-hint",
6254
+ children: t("serverNameImmutable")
6255
+ })
6256
+ ]
6257
+ }),
6258
+ transport === "stdio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6260
+ className: "lm-field",
6261
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6262
+ className: "lm-label",
6263
+ children: t("command")
6264
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6265
+ className: "lm-input",
6266
+ value: command,
6267
+ onChange: (e) => setCommand(e.target.value)
6150
6268
  })]
6151
6269
  }),
6152
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6153
- className: "lm-hint",
6154
- children: transport === "stdio" ? t("transportHintStdio") : t("transportHintHttp")
6155
- })
6156
- ]
6157
- }),
6158
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6159
- className: "lm-field",
6160
- children: [
6161
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6162
- className: "lm-label",
6163
- children: t("serverName")
6270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6271
+ className: "lm-field",
6272
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6273
+ className: "lm-label",
6274
+ children: t("args")
6275
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6276
+ className: "lm-input",
6277
+ value: args,
6278
+ onChange: (e) => setArgs(e.target.value)
6279
+ })]
6164
6280
  }),
6165
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6166
- className: "lm-input",
6167
- value: serverName,
6168
- disabled: editMcp !== void 0,
6169
- onChange: (e) => setServerName(e.target.value)
6281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6282
+ className: "lm-field",
6283
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6284
+ className: "lm-label",
6285
+ children: t("cwd")
6286
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6287
+ className: "lm-input",
6288
+ value: cwd,
6289
+ onChange: (e) => setCwd(e.target.value)
6290
+ })]
6170
6291
  }),
6171
- editMcp !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6172
- className: "lm-hint",
6173
- children: t("serverNameImmutable")
6292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6293
+ className: "lm-field",
6294
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6295
+ className: "lm-label",
6296
+ children: t("env")
6297
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6298
+ className: "lm-input",
6299
+ rows: 3,
6300
+ value: env,
6301
+ onChange: (e) => setEnv(e.target.value)
6302
+ })]
6174
6303
  })
6175
- ]
6176
- }),
6177
- transport === "stdio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6178
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6304
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6179
6305
  className: "lm-field",
6180
6306
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6181
6307
  className: "lm-label",
6182
- children: t("command")
6308
+ children: t("url")
6183
6309
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6184
6310
  className: "lm-input",
6185
- value: command,
6186
- onChange: (e) => setCommand(e.target.value)
6311
+ value: url,
6312
+ onChange: (e) => setUrl(e.target.value)
6187
6313
  })]
6188
- }),
6314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6315
+ className: "lm-field",
6316
+ children: [
6317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6318
+ className: "lm-label",
6319
+ children: t("headers")
6320
+ }),
6321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6322
+ className: "lm-input",
6323
+ rows: 4,
6324
+ value: headers,
6325
+ onChange: (e) => setHeaders(e.target.value)
6326
+ }),
6327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6328
+ className: "lm-hint",
6329
+ children: t("headersHint")
6330
+ })
6331
+ ]
6332
+ })] }),
6189
6333
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6190
6334
  className: "lm-field",
6191
6335
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6192
6336
  className: "lm-label",
6193
- children: t("args")
6337
+ children: t("timeout")
6194
6338
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6195
6339
  className: "lm-input",
6196
- value: args,
6197
- onChange: (e) => setArgs(e.target.value)
6340
+ value: timeoutSec,
6341
+ placeholder: "60",
6342
+ onChange: (e) => setTimeoutSec(e.target.value)
6198
6343
  })]
6199
6344
  }),
6345
+ renderActions(() => void submitMcp())
6346
+ ]
6347
+ })
6348
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6349
+ className: "lm-block",
6350
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6351
+ className: "lm-form",
6352
+ children: [
6200
6353
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6201
6354
  className: "lm-field",
6202
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6203
- className: "lm-label",
6204
- children: t("cwd")
6205
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6206
- className: "lm-input",
6207
- value: cwd,
6208
- onChange: (e) => setCwd(e.target.value)
6209
- })]
6355
+ children: [
6356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6357
+ className: "lm-label",
6358
+ children: t("skillRoot")
6359
+ }),
6360
+ editSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6361
+ className: "lm-input",
6362
+ value: t(editSkill.root === "project" ? "skillRootProject" : "skillRootUser"),
6363
+ disabled: true
6364
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6365
+ className: "lm-input",
6366
+ value: skillRoot,
6367
+ onChange: (e) => setSkillRoot(e.target.value === "project" ? "project" : "user"),
6368
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6369
+ value: "user",
6370
+ children: t("skillRootUser")
6371
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6372
+ value: "project",
6373
+ children: t("skillRootProject")
6374
+ })]
6375
+ }),
6376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6377
+ className: "lm-hint",
6378
+ children: skillRoot === "project" || editSkill?.root === "project" ? t("skillRootHintProject") : t("skillRootHintUser")
6379
+ })
6380
+ ]
6210
6381
  }),
6211
6382
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6212
6383
  className: "lm-field",
6213
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6214
- className: "lm-label",
6215
- children: t("env")
6216
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6217
- className: "lm-input",
6218
- rows: 3,
6219
- value: env,
6220
- onChange: (e) => setEnv(e.target.value)
6221
- })]
6222
- })
6223
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6224
- className: "lm-field",
6225
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6226
- className: "lm-label",
6227
- children: t("url")
6228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6229
- className: "lm-input",
6230
- value: url,
6231
- onChange: (e) => setUrl(e.target.value)
6232
- })]
6233
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6234
- className: "lm-field",
6235
- children: [
6236
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6237
- className: "lm-label",
6238
- children: t("headers")
6239
- }),
6240
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6241
- className: "lm-input",
6242
- rows: 4,
6243
- value: headers,
6244
- onChange: (e) => setHeaders(e.target.value)
6245
- }),
6246
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6247
- className: "lm-hint",
6248
- children: t("headersHint")
6249
- })
6250
- ]
6251
- })] }),
6252
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6253
- className: "lm-field",
6254
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6255
- className: "lm-label",
6256
- children: t("timeout")
6257
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6258
- className: "lm-input",
6259
- value: timeoutSec,
6260
- placeholder: "60",
6261
- onChange: (e) => setTimeoutSec(e.target.value)
6262
- })]
6263
- }),
6264
- renderActions(() => void submitMcp())
6265
- ]
6266
- })
6267
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6268
- className: "lm-block",
6269
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6270
- className: "lm-form",
6271
- children: [
6272
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6273
- className: "lm-field",
6274
- children: [
6275
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6276
- className: "lm-label",
6277
- children: t("skillRoot")
6278
- }),
6279
- editSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6280
- className: "lm-input",
6281
- value: t(editSkill.root === "project" ? "skillRootProject" : "skillRootUser"),
6282
- disabled: true
6283
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6284
- className: "lm-input",
6285
- value: skillRoot,
6286
- onChange: (e) => setSkillRoot(e.target.value === "project" ? "project" : "user"),
6287
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6288
- value: "user",
6289
- children: t("skillRootUser")
6290
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6291
- value: "project",
6292
- children: t("skillRootProject")
6293
- })]
6294
- }),
6295
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6296
- className: "lm-hint",
6297
- children: skillRoot === "project" || editSkill?.root === "project" ? t("skillRootHintProject") : t("skillRootHintUser")
6298
- })
6299
- ]
6300
- }),
6301
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6302
- className: "lm-field",
6303
- children: [
6304
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6305
- className: "lm-label",
6306
- children: t("skillDirPath")
6307
- }),
6308
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6309
- className: "lm-input",
6310
- value: skillDir,
6311
- onChange: (e) => setSkillDir(e.target.value)
6312
- }),
6313
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6314
- className: "lm-hint",
6315
- children: t("skillDirHint")
6316
- })
6317
- ]
6318
- }),
6319
- editSkill === void 0 && skillRoot === "project" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6320
- className: "lm-field",
6321
- children: [
6322
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6323
- className: "lm-label",
6324
- children: t("skillProjectPath")
6325
- }),
6326
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6327
- className: "lm-input",
6328
- value: projectPath,
6329
- onChange: (e) => setProjectPath(e.target.value)
6330
- }),
6331
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6332
- className: "lm-hint",
6333
- children: t("skillProjectPathHint")
6334
- })
6335
- ]
6336
- }),
6337
- renderActions(() => void submitSkill())
6338
- ]
6384
+ children: [
6385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6386
+ className: "lm-label",
6387
+ children: t("skillDirPath")
6388
+ }),
6389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6390
+ className: "lm-input",
6391
+ value: skillDir,
6392
+ onChange: (e) => setSkillDir(e.target.value)
6393
+ }),
6394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6395
+ className: "lm-hint",
6396
+ children: t("skillDirHint")
6397
+ })
6398
+ ]
6399
+ }),
6400
+ editSkill === void 0 && skillRoot === "project" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6401
+ className: "lm-field",
6402
+ children: [
6403
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6404
+ className: "lm-label",
6405
+ children: t("skillProjectPath")
6406
+ }),
6407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6408
+ className: "lm-input",
6409
+ value: projectPath,
6410
+ onChange: (e) => setProjectPath(e.target.value)
6411
+ }),
6412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6413
+ className: "lm-hint",
6414
+ children: t("skillProjectPathHint")
6415
+ })
6416
+ ]
6417
+ }),
6418
+ renderActions(() => void submitSkill())
6419
+ ]
6420
+ })
6339
6421
  })
6340
- })]
6422
+ ]
6341
6423
  })
6342
6424
  ]
6343
6425
  })
@@ -6361,6 +6443,210 @@ const PROJECT_SKILL_SOURCES = /* @__PURE__ */ new Set(["project-dsh", "project-a
6361
6443
  */
6362
6444
  const ADOPTABLE_SKILL_SOURCES = /* @__PURE__ */ new Set(["user-agents", "custom"]);
6363
6445
  //#endregion
6446
+ //#region src/client/skillGroups.ts
6447
+ /**
6448
+ * What the capability list decides about *which* rows it shows, kept out of
6449
+ * `CapabilitySection.tsx` as pure functions.
6450
+ *
6451
+ * The split is deliberate: the grouping rules are where this plugin got its one
6452
+ * real mis-grouping (preset skills filed under 全局技能), and they are also the
6453
+ * only part of the client a test can reach — this repo has no DOM harness, so
6454
+ * anything that lives inside the component can only be checked by hand.
6455
+ */
6456
+ /** Group tools by server name; skills stay flat (no server), both name-sorted. */
6457
+ function groupRows(rows) {
6458
+ const byServer = /* @__PURE__ */ new Map();
6459
+ const skills = [];
6460
+ for (const row of rows) {
6461
+ if (row.kind === "skill") {
6462
+ skills.push(row);
6463
+ continue;
6464
+ }
6465
+ const server = row.server ?? "built-in";
6466
+ const list = byServer.get(server);
6467
+ if (list === void 0) byServer.set(server, [row]);
6468
+ else list.push(row);
6469
+ }
6470
+ return {
6471
+ servers: [...byServer.entries()].map(([server, tools]) => ({
6472
+ server,
6473
+ tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
6474
+ })).sort((a, b) => a.server.localeCompare(b.server)),
6475
+ skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
6476
+ };
6477
+ }
6478
+ /**
6479
+ * Compile the box. A `/…/` wrapper is optional — it is stripped when present, so
6480
+ * both `cordis|ptc` and `/cordis|ptc/` are read as patterns — and it keeps the
6481
+ * common case of pasting a slash-delimited pattern from working.
6482
+ */
6483
+ function compileNameFilter(input) {
6484
+ const raw = input.trim();
6485
+ const needle = raw.toLowerCase();
6486
+ if (raw === "") return {
6487
+ needle,
6488
+ regex: false,
6489
+ test: () => true
6490
+ };
6491
+ const pattern = raw.length > 1 && raw.startsWith("/") && raw.endsWith("/") ? raw.slice(1, -1) : raw;
6492
+ try {
6493
+ const compiled = new RegExp(pattern, "i");
6494
+ return {
6495
+ needle,
6496
+ regex: true,
6497
+ test: (values) => values.some((v) => v !== void 0 && compiled.test(v))
6498
+ };
6499
+ } catch {
6500
+ return {
6501
+ needle,
6502
+ regex: false,
6503
+ test: (values) => values.some((v) => v !== void 0 && v.toLowerCase().includes(needle))
6504
+ };
6505
+ }
6506
+ }
6507
+ /**
6508
+ * Everything a row is matched against: its own name, plus the dimensions that
6509
+ * name the group it sits in — the MCP server (or the label the built-in group is
6510
+ * drawn with), the skill's source root, and the preset that ships it.
6511
+ *
6512
+ * Group headings are labels rather than row fields, which is exactly why typing
6513
+ * 系统 used to find nothing: the server id is `built-in`, and 系统内置 is only how
6514
+ * that id is painted. Both spellings are searchable now.
6515
+ */
6516
+ function searchableText(row, labels = {}) {
6517
+ const values = [row.name];
6518
+ if (row.server !== void 0) {
6519
+ values.push(row.server);
6520
+ if (row.server === "built-in") values.push(labels.builtIn);
6521
+ }
6522
+ if (row.source !== void 0) values.push(row.source, labels.source?.(row.source));
6523
+ values.push(row.preset);
6524
+ return values.filter((v) => v !== void 0 && v !== "");
6525
+ }
6526
+ /**
6527
+ * Narrow a list by the filter. Applied *before* grouping so a server header's
6528
+ * own counts describe the rows actually listed under it.
6529
+ */
6530
+ function filterRows(rows, filter, labels = {}) {
6531
+ if (filter.needle === "") return [...rows];
6532
+ return rows.filter((row) => filter.test(searchableText(row, labels)));
6533
+ }
6534
+ /**
6535
+ * Split skill rows across the three sub-tabs.
6536
+ *
6537
+ * Preset skills are split off by their `preset` field and never by `source`: a
6538
+ * preset's own `skill-filesystem` labels its skills `custom`, exactly like a
6539
+ * user-configured `customSkillDirs` entry, so a source-based rule drags both
6540
+ * into 全局技能 — filing a preset asset as global is precisely the mis-grouping
6541
+ * this split exists to fix. Rows keep the order they arrive in, so callers that
6542
+ * pass name-sorted rows get name-sorted groups.
6543
+ */
6544
+ function splitSkillGroups(skills) {
6545
+ const presetSkills = [];
6546
+ const projectSkills = [];
6547
+ const globalSkills = [];
6548
+ const presetGroups = [];
6549
+ for (const skill of skills) {
6550
+ const preset = skill.preset;
6551
+ if (preset !== void 0) {
6552
+ presetSkills.push(skill);
6553
+ const group = presetGroups.find(([id]) => id === preset);
6554
+ if (group === void 0) presetGroups.push([preset, [skill]]);
6555
+ else group[1].push(skill);
6556
+ continue;
6557
+ }
6558
+ if (PROJECT_SKILL_SOURCES.has(skill.source ?? "")) projectSkills.push(skill);
6559
+ else globalSkills.push(skill);
6560
+ }
6561
+ presetGroups.sort(([a], [b]) => a.localeCompare(b));
6562
+ return {
6563
+ presetSkills,
6564
+ presetGroups,
6565
+ projectSkills,
6566
+ globalSkills
6567
+ };
6568
+ }
6569
+ /** Rows in one tier. Counts the *displayed* rows, not the whole catalog. */
6570
+ function countByClass(rows, cls) {
6571
+ return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
6572
+ }
6573
+ //#endregion
6574
+ //#region src/client/revalidate.ts
6575
+ /**
6576
+ * When to re-read the catalog, and how often to look — the decisions behind the
6577
+ * management page keeping itself fresh, kept as pure functions.
6578
+ *
6579
+ * Wiring lives in `CapabilitySection.tsx` (timers, the `visibilitychange`
6580
+ * listener, the two subscriptions); everything that could be got wrong lives
6581
+ * here, where a test can reach it. This repo has no DOM harness.
6582
+ */
6583
+ /**
6584
+ * How often a visible, idle page asks the host for its catalog version.
6585
+ *
6586
+ * This is the resolution of the one case nothing on the wire announces — a file
6587
+ * dropped into an already-registered skill directory — so it doubles as "how
6588
+ * long will I stare at a stale list after an agent changes something next to
6589
+ * me". 5s keeps that from being annoying; the cost is one tiny request per tick,
6590
+ * and only while this settings page is on screen.
6591
+ */
6592
+ const POLL_INTERVAL_MS = 5e3;
6593
+ /** Ceiling for the failure backoff, so a long outage does not spin. */
6594
+ const POLL_BACKOFF_MAX_MS = 6e4;
6595
+ /**
6596
+ * How long one version sample may take before it is treated as a failure.
6597
+ *
6598
+ * Without this, a request that neither resolves nor rejects would end the poll
6599
+ * for good: the next tick is only scheduled after the current one settles, and
6600
+ * nothing else restarts the effect while the page stays visible and idle.
6601
+ */
6602
+ const POLL_SAMPLE_TIMEOUT_MS = 15e3;
6603
+ /**
6604
+ * Delay before the next version poll.
6605
+ *
6606
+ * `failures` is the run of consecutive failures ending at this decision: 0
6607
+ * means the last sample succeeded and the plain interval applies, otherwise the
6608
+ * delay doubles until it hits the ceiling. This is what turns "the carrier
6609
+ * died" from a hot loop into a slow retry that heals itself when it comes back.
6610
+ */
6611
+ function pollDelayMs(failures) {
6612
+ if (!Number.isFinite(failures) || failures <= 0) return POLL_INTERVAL_MS;
6613
+ return Math.min(POLL_INTERVAL_MS * 2 ** failures, POLL_BACKOFF_MAX_MS);
6614
+ }
6615
+ /**
6616
+ * Whether a version sample means the catalog moved under the page.
6617
+ *
6618
+ * Deliberately `!==` rather than `>`: the counter is in-process, so it restarts
6619
+ * from zero when the plugin reloads, and a plain "greater than" test would sit
6620
+ * there believing a fresh host was the same host. `undefined` (nothing sampled
6621
+ * yet) only records — the page has just read the snapshot, and re-reading it
6622
+ * because it has no baseline would be busywork.
6623
+ */
6624
+ function versionChanged(seen, current) {
6625
+ return seen !== void 0 && seen !== current;
6626
+ }
6627
+ /**
6628
+ * The value of `work`, or a rejection if it has not settled within `ms`.
6629
+ *
6630
+ * Used so a carrier that accepts a request and then goes quiet degrades into
6631
+ * the same retry path as a carrier that refuses one.
6632
+ */
6633
+ function withDeadline(work, ms) {
6634
+ return new Promise((resolve, reject) => {
6635
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${ms}ms`)), ms);
6636
+ work.then((value) => {
6637
+ clearTimeout(timer);
6638
+ resolve(value);
6639
+ }, (error) => {
6640
+ clearTimeout(timer);
6641
+ reject(error);
6642
+ });
6643
+ });
6644
+ }
6645
+ /** Whether a signal may act now, or has to be parked until the page is idle. */
6646
+ function canRevalidate(gate) {
6647
+ return gate.visible && gate.ready && !gate.busy && !gate.rebuilding;
6648
+ }
6649
+ //#endregion
6364
6650
  //#region src/client/CapabilitySection.tsx
6365
6651
  /**
6366
6652
  * ⚠️ VERIFIED AGAINST REAL rc.8 CLIENT API.
@@ -6382,6 +6668,8 @@ const ADOPTABLE_SKILL_SOURCES = /* @__PURE__ */ new Set(["user-agents", "custom"
6382
6668
  * with flat skill rows carrying the same clickable class
6383
6669
  * chip plus a directory tree / file preview
6384
6670
  */
6671
+ const chevronIcons = _deepseek_ai_dsh_client_ui_primitives;
6672
+ const IconTriangleRightFill = chevronIcons.IconTriangleRightFillMedium ?? chevronIcons.IconTriangleRightFill14;
6385
6673
  const CLASS_KEYS = [
6386
6674
  "resident",
6387
6675
  "on-demand",
@@ -6404,14 +6692,25 @@ const CSS_ID = "capability-menu-section-css";
6404
6692
  const CSS = `
6405
6693
  .mc-section{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary)}
6406
6694
  .mc-heading{margin:0;font-size:18px;font-weight:600}
6695
+ .mc-heading-row{display:flex;align-items:baseline;gap:8px;min-width:0;flex-wrap:wrap}
6696
+ .mc-heading-meta{display:flex;align-items:baseline;gap:8px;min-width:0}
6697
+ .mc-feedback-link{margin-left:auto;flex:none;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px;white-space:nowrap;text-decoration:none}
6698
+ .mc-feedback-link:hover,.mc-feedback-link:focus-visible{color:#527a9c;text-decoration:underline;text-underline-offset:3px}
6699
+ .mc-feedback-link:focus-visible{outline:2px solid #527a9c;outline-offset:2px;border-radius:2px}
6700
+ body[data-ds-dark-theme] .mc-feedback-link:hover,body[data-ds-dark-theme] .mc-feedback-link:focus-visible{color:#96b6d1}
6701
+ body[data-ds-dark-theme] .mc-feedback-link:focus-visible{outline-color:#96b6d1}
6702
+ .mc-package-name{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}
6407
6703
  .mc-desc{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px}
6408
6704
  /* 说明行右侧放只读文档入口:把它从头部的计数行挪出来,计数多(Tools 常驻 ·
6409
6705
  167)时那一行不再因此折行。 */
6410
6706
  .mc-desc-row{display:flex;align-items:center;justify-content:space-between;gap:12px}
6411
6707
  .mc-desc-row .mc-desc{flex:1 1 auto;min-width:0}
6708
+ .mc-version{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;white-space:nowrap}
6412
6709
  .mc-summary{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end;align-items:center;padding-bottom:8px}
6413
6710
  .mc-catalog-btn{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:20px;padding:0 10px;cursor:pointer;white-space:nowrap}
6414
6711
  .mc-catalog-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
6712
+ /* 忙碌态靠变淡表示,而不是换文案:换文案会改按钮宽度,把这行挤到第二行。 */
6713
+ .mc-catalog-btn:disabled{cursor:default;opacity:.6}
6415
6714
  .mc-catalog-tabs{padding:8px 16px 0}
6416
6715
  .mc-catalog-path{padding:8px 16px 0;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);word-break:break-all}
6417
6716
  .mc-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:20px;white-space:nowrap}
@@ -6435,6 +6734,15 @@ body[data-ds-dark-theme] .mc-chip--resident{color:#96b6d1}
6435
6734
  body[data-ds-dark-theme] .mc-chip--on-demand{color:#d4b26b}
6436
6735
  body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
6437
6736
  .mc-tabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;justify-content:space-between;gap:22px}
6737
+ /* 名字过滤框:两个 tab 共用一条,宽度占满,与下方列表左对齐。
6738
+ 它属于上面的主 tab 栏,不属于下面的列表,所以两侧都把 .mc-section 的 12px
6739
+ 栏距收掉一部分:下划线—6px—输入框—8px—全局/项目 子页签文字(后者里的 4px
6740
+ 是子页签自己的 padding-top)。改之前是 12+12=24px 和 12+4=16px,读起来像
6741
+ 两个不相干的块。 */
6742
+ .mc-filter{display:flex;padding-top:0;margin-top:-6px;margin-bottom:-8px}
6743
+ .mc-filter input{box-sizing:border-box;width:100%;min-width:0;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit;font-size:13px;line-height:18px}
6744
+ .mc-filter input:hover{border-color:var(--dsw-alias-border-l3)}
6745
+ .mc-filter input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-color:transparent}
6438
6746
  /* Skills sub-tab bar (全局技能/项目技能): same underline chrome, no right-side summary. */
6439
6747
  .mc-subtabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px}
6440
6748
  .mc-tab-group{display:flex;align-items:flex-end;gap:22px}
@@ -6442,13 +6750,17 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
6442
6750
  .mc-tab:hover,.mc-tab[data-active=true]{color:var(--dsw-alias-label-primary)}
6443
6751
  .mc-tab[data-active=true]:after,.mc-tab:focus-visible:after{background:var(--dsw-alias-label-primary);content:"";border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-1px;left:0;right:0}
6444
6752
  .mc-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary);border-radius:2px}
6445
- .mc-panel{min-width:0;padding-top:12px}
6446
- /* Skills panel: its first row is the 全局技能/项目技能 sub-tab bar, which sits
6447
- directly under the main tab bar. It is a second level of the same control, so
6448
- the two bars have to read as one stack: no panel padding at all, and the
6449
- sub-tabs keep a token top padding instead of the 7px they share with the
6450
- primary tabs. Together the label sits ~13px under the main label rather than
6451
- ~20px, with the main bar's rule between them. */
6753
+ /* 面板的第一行必须和另一个 tab 的第一行从过滤框下沿出发走同样远,否则切 tab 时
6754
+ 首行会跳一下。这里留 4px,加上 .mc-section 的 4px 栏距(过滤框那侧被 -8px 收
6755
+ 掉后剩 4px)= 8px,正好是 Skills 面板子页签文字的位置(4px 栏距 + 子页签自己
6756
+ 的 4px padding-top)。原来这里是 12px,Tools 的首个分组因此落在过滤框下 16px
6757
+ 处,比「全局技能」那行低一半。 */
6758
+ .mc-panel{min-width:0;padding-top:4px}
6759
+ /* Skills panel: its first row is the 全局技能/项目技能 sub-tab bar, and that bar
6760
+ belongs to the header stack above it (rule → name filter → sub-tabs) rather
6761
+ than to the list below. So the panel adds no padding of its own, and the
6762
+ sub-tab labels keep a 4px top padding instead of the 7px they share with the
6763
+ primary tabs. */
6452
6764
  .mc-panel--tight{padding-top:0}
6453
6765
  .mc-panel--tight .mc-subtabs .mc-tab{padding-top:4px}
6454
6766
  .mc-panel-inner{display:flex;flex-direction:column;gap:14px}
@@ -6490,7 +6802,7 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
6490
6802
  .mc-error p{margin:0}
6491
6803
  .mc-error p.mc-error-hint{margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
6492
6804
  .mc-error-actions{display:flex;justify-content:flex-end;margin-top:10px}
6493
- .mc-notice{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:13px}
6805
+ .mc-notice{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:13px}
6494
6806
  .mc-skill{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
6495
6807
  .mc-skill-row{box-sizing:border-box;display:flex;align-items:center;gap:10px;width:100%;min-width:0;padding:10px 12px;background:0 0;border:0;color:inherit;font:inherit;text-align:left;cursor:pointer}
6496
6808
  .mc-skill-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -6508,6 +6820,9 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
6508
6820
  .mc-confirm-path{font-family:var(--dsw-font-markdown-code-block-font-family);word-break:break-all}
6509
6821
  .mc-confirm-actions{display:flex;justify-content:flex-end;gap:8px}
6510
6822
  .mc-skill-body{border-top:1px solid var(--dsw-alias-border-l1);padding:8px 12px 12px}
6823
+ /* 预设技能组:预设 id 是页签之下的分组小标题,不是新一级页签。 */
6824
+ .mc-skill-group{display:flex;flex-direction:column;gap:8px;min-width:0}
6825
+ .mc-skill-group-title{font-size:12px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-secondary);padding:0 2px}
6511
6826
  .mc-tree{display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:20px}
6512
6827
  .mc-tree-row{display:flex;align-items:center;gap:8px;padding:3px 4px;border-radius:6px;cursor:pointer;min-width:0}
6513
6828
  .mc-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -6521,7 +6836,13 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
6521
6836
  .mc-preview-title{font-size:13px;line-height:20px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
6522
6837
  .mc-preview-close{flex:none;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
6523
6838
  .mc-preview-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
6524
- .mc-preview-body{overflow:auto;flex:1 1 auto;min-height:0;padding:14px 16px;font-family:var(--dsw-font-markdown-code-block-font-family);font-size:12px;line-height:20px;white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-primary)}
6839
+ .mc-preview-body{overflow:auto;flex:1 1 auto;min-height:0;padding:14px 16px;color:var(--dsw-alias-label-primary)}
6840
+ .mc-preview-body>pre,.mc-preview-plain{margin:0;font-family:var(--dsw-font-markdown-code-block-font-family);font-size:12px;line-height:20px;white-space:pre-wrap;overflow-wrap:anywhere}
6841
+ .mc-tool-description{padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid var(--dsw-alias-border-l1)}
6842
+ .mc-frontmatter{width:100%;margin:0 0 20px;border-collapse:collapse;table-layout:fixed;font-size:13px;line-height:20px}
6843
+ .mc-frontmatter th,.mc-frontmatter td{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);vertical-align:top;text-align:left}
6844
+ .mc-frontmatter th{width:130px;font-weight:500;color:var(--dsw-alias-label-secondary)}
6845
+ .mc-frontmatter td{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;white-space:pre-wrap}
6525
6846
  .mc-preview-hint{padding:16px;text-align:center;font-size:13px;color:var(--dsw-alias-label-tertiary)}
6526
6847
  `;
6527
6848
  if (typeof document !== "undefined" && document.querySelector(`style[data-css-id="${CSS_ID}"]`) === null) {
@@ -6530,6 +6851,78 @@ if (typeof document !== "undefined" && document.querySelector(`style[data-css-id
6530
6851
  tag.textContent = CSS;
6531
6852
  document.head.appendChild(tag);
6532
6853
  }
6854
+ /** Reuse dsh's safe Markdown renderer in tool descriptions and skill files. */
6855
+ function MarkdownPreview(props) {
6856
+ const { text, t } = props;
6857
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
6858
+ text,
6859
+ labels: {
6860
+ code: {
6861
+ copyLabel: t("markdownCopy"),
6862
+ copiedLabel: t("markdownCopied"),
6863
+ toolbarLabels: {
6864
+ codeLabel: t("markdownCode"),
6865
+ wrapLabel: t("markdownWrap"),
6866
+ unwrapLabel: t("markdownUnwrap")
6867
+ }
6868
+ },
6869
+ footnotes: t("markdownFootnotes")
6870
+ }
6871
+ });
6872
+ }
6873
+ /** Parse the simple property lines shown by GitHub above a Markdown document. */
6874
+ function parseFrontmatterProperties(source) {
6875
+ const properties = [];
6876
+ for (const line of source.split(/\r?\n/)) {
6877
+ const field = /^([A-Za-z0-9_.-]+):(?:[ \t]*(.*))?$/.exec(line);
6878
+ if (field !== null) {
6879
+ properties.push({
6880
+ name: field[1],
6881
+ value: field[2] ?? ""
6882
+ });
6883
+ continue;
6884
+ }
6885
+ const continuation = /^\s+(.+)$/.exec(line);
6886
+ if (continuation !== null && properties.length > 0) {
6887
+ const last = properties[properties.length - 1];
6888
+ last.value = last.value.length === 0 ? continuation[1] : `${last.value}\n${continuation[1]}`;
6889
+ }
6890
+ }
6891
+ return properties.map((property) => ({
6892
+ ...property,
6893
+ value: property.value.trimEnd()
6894
+ }));
6895
+ }
6896
+ /** Split YAML properties from the Markdown body. */
6897
+ function splitMarkdownFrontmatter(text) {
6898
+ const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
6899
+ if (match === null) return { body: text };
6900
+ return {
6901
+ frontmatter: parseFrontmatterProperties(match[1] ?? ""),
6902
+ body: text.slice(match[0].length)
6903
+ };
6904
+ }
6905
+ function MarkdownFilePreview(props) {
6906
+ const { frontmatter, body } = splitMarkdownFrontmatter(props.text);
6907
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [frontmatter !== void 0 && frontmatter.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("table", {
6908
+ className: "mc-frontmatter",
6909
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: frontmatter.map((property) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
6910
+ scope: "row",
6911
+ children: property.name
6912
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: property.value })] }, property.name)) })
6913
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownPreview, {
6914
+ text: body,
6915
+ t: props.t
6916
+ })] });
6917
+ }
6918
+ /**
6919
+ * Which build of the plugin is on screen. Several rounds of "is my profile
6920
+ * running the version I just installed?" had to be answered by grepping
6921
+ * `node_modules`, because the panel said nothing about itself. The value is
6922
+ * inlined by `tsdown` (see `src/client/env.d.ts`).
6923
+ */
6924
+ const PLUGIN_VERSION = "0.1.5";
6925
+ const PLUGIN_TITLE = `capability-menu v${PLUGIN_VERSION}`;
6533
6926
  /**
6534
6927
  * dsh's skill source labels, shown on rows this plugin cannot edit — so "why is
6535
6928
  * there no 编辑 button" has a visible answer. Labels that name one directory are
@@ -6555,32 +6948,22 @@ function sourceLabel(source, t) {
6555
6948
  const key = SOURCE_LABEL_KEYS[source];
6556
6949
  return key === void 0 ? source : t(key);
6557
6950
  }
6558
- function groupRows(rows) {
6559
- const byServer = /* @__PURE__ */ new Map();
6560
- const skills = [];
6561
- for (const row of rows) {
6562
- if (row.kind === "skill") {
6563
- skills.push(row);
6564
- continue;
6565
- }
6566
- const server = row.server ?? "built-in";
6567
- const list = byServer.get(server);
6568
- if (list === void 0) byServer.set(server, [row]);
6569
- else list.push(row);
6570
- }
6571
- return {
6572
- servers: [...byServer.entries()].map(([server, tools]) => ({
6573
- server,
6574
- tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
6575
- })).sort((a, b) => a.server.localeCompare(b.server)),
6576
- skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
6577
- };
6578
- }
6579
- function countByClass(rows, cls) {
6580
- return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
6951
+ /**
6952
+ * Whether the document is visible. A hidden settings tab does no polling and
6953
+ * takes no re-reads; coming back is itself a revalidation trigger.
6954
+ */
6955
+ function useDocumentVisible() {
6956
+ const [visible, setVisible] = (0, react.useState)(true);
6957
+ (0, react.useEffect)(() => {
6958
+ const update = () => setVisible(document.visibilityState !== "hidden");
6959
+ update();
6960
+ document.addEventListener("visibilitychange", update);
6961
+ return () => document.removeEventListener("visibilitychange", update);
6962
+ }, []);
6963
+ return visible;
6581
6964
  }
6582
6965
  function CapabilitySection(props) {
6583
- const { remote, t, mountError } = props;
6966
+ const { remote, t, mountError, subscribeSignals } = props;
6584
6967
  const [state, setState] = (0, react.useState)(() => {
6585
6968
  const cached = cachedSnapshot();
6586
6969
  return cached === void 0 ? { status: "loading" } : {
@@ -6590,6 +6973,8 @@ function CapabilitySection(props) {
6590
6973
  });
6591
6974
  const [busy, setBusy] = (0, react.useState)(false);
6592
6975
  const [notice, setNotice] = (0, react.useState)(null);
6976
+ const [retryMcp, setRetryMcp] = (0, react.useState)(null);
6977
+ const queuedMcpRefresh = (0, react.useRef)(null);
6593
6978
  const [openServers, setOpenServers] = (0, react.useState)(/* @__PURE__ */ new Set());
6594
6979
  const [activeTab, setActiveTab] = (0, react.useState)("tools");
6595
6980
  const reload = (0, react.useCallback)(async () => {
@@ -6625,6 +7010,7 @@ function CapabilitySection(props) {
6625
7010
  const refreshCatalog = (0, react.useCallback)(async () => {
6626
7011
  if (refreshing) return;
6627
7012
  setRefreshing(true);
7013
+ setRetryMcp(null);
6628
7014
  try {
6629
7015
  if (remote !== void 0) unwrap(await remote.refresh(), "capabilityPolicy.refresh");
6630
7016
  await reload();
@@ -6641,6 +7027,61 @@ function CapabilitySection(props) {
6641
7027
  reload,
6642
7028
  t
6643
7029
  ]);
7030
+ /**
7031
+ * A new MCP row is persisted before dsh applies it and registers its tools.
7032
+ * Wait for that asynchronous host reload instead of taking one snapshot too
7033
+ * early and requiring the operator to leave and reopen this settings page.
7034
+ */
7035
+ const refreshRegisteredMcp = (0, react.useCallback)(async (serverName) => {
7036
+ if (refreshing) {
7037
+ queuedMcpRefresh.current = serverName;
7038
+ return;
7039
+ }
7040
+ if (remote === void 0) return;
7041
+ setRefreshing(true);
7042
+ setRetryMcp(null);
7043
+ try {
7044
+ unwrap(await remote.refresh(), "capabilityPolicy.refresh");
7045
+ const snapshot = await waitForMcpTools(remote, serverName, [
7046
+ 250,
7047
+ 500,
7048
+ 750,
7049
+ 1e3,
7050
+ 1e3,
7051
+ 1e3,
7052
+ 1e3,
7053
+ 1e3,
7054
+ 1e3
7055
+ ]);
7056
+ if (snapshot !== void 0) {
7057
+ setState({
7058
+ status: "ready",
7059
+ snapshot: rememberSnapshot(snapshot)
7060
+ });
7061
+ setNotice(null);
7062
+ setRetryMcp(null);
7063
+ } else {
7064
+ setNotice(t("mcpToolsPending", { name: serverName }));
7065
+ setRetryMcp(serverName);
7066
+ }
7067
+ } catch (e) {
7068
+ console.error("[capability-menu] MCP refresh failed:", e);
7069
+ setNotice(t("refreshFailed"));
7070
+ setRetryMcp(serverName);
7071
+ } finally {
7072
+ setRefreshing(false);
7073
+ }
7074
+ }, [
7075
+ refreshing,
7076
+ remote,
7077
+ t
7078
+ ]);
7079
+ (0, react.useEffect)(() => {
7080
+ if (refreshing || queuedMcpRefresh.current === null) return;
7081
+ const serverName = queuedMcpRefresh.current;
7082
+ queuedMcpRefresh.current = null;
7083
+ refreshRegisteredMcp(serverName);
7084
+ }, [refreshing, refreshRegisteredMcp]);
6644
7085
  const toggleServer = (0, react.useCallback)((server) => {
6645
7086
  setOpenServers((prev) => {
6646
7087
  const next = new Set(prev);
@@ -6649,6 +7090,84 @@ function CapabilitySection(props) {
6649
7090
  return next;
6650
7091
  });
6651
7092
  }, []);
7093
+ const visible = useDocumentVisible();
7094
+ const [pendingSignal, setPendingSignal] = (0, react.useState)(false);
7095
+ const seenVersion = (0, react.useRef)(void 0);
7096
+ const pollFailures = (0, react.useRef)(0);
7097
+ const gate = (0, react.useRef)({
7098
+ visible: true,
7099
+ ready: false,
7100
+ busy: false,
7101
+ rebuilding: false
7102
+ });
7103
+ (0, react.useEffect)(() => {
7104
+ gate.current = {
7105
+ visible,
7106
+ ready: state.status === "ready",
7107
+ busy,
7108
+ rebuilding: refreshing
7109
+ };
7110
+ });
7111
+ /**
7112
+ * A signal that the host catalog may have moved. Runs now when the page is
7113
+ * idle; otherwise parks until it is, so a signal that lands mid-click is
7114
+ * never dropped (and never repaints the row under the cursor).
7115
+ */
7116
+ const onExternalSignal = (0, react.useCallback)(() => {
7117
+ if (canRevalidate(gate.current)) refreshCatalog();
7118
+ else setPendingSignal(true);
7119
+ }, [refreshCatalog]);
7120
+ (0, react.useEffect)(() => {
7121
+ return subscribeSignals?.(onExternalSignal);
7122
+ }, [subscribeSignals, onExternalSignal]);
7123
+ (0, react.useEffect)(() => {
7124
+ if (!pendingSignal) return;
7125
+ if (!canRevalidate({
7126
+ visible,
7127
+ ready: state.status === "ready",
7128
+ busy,
7129
+ rebuilding: refreshing
7130
+ })) return;
7131
+ setPendingSignal(false);
7132
+ refreshCatalog();
7133
+ }, [
7134
+ pendingSignal,
7135
+ visible,
7136
+ state.status,
7137
+ busy,
7138
+ refreshing,
7139
+ refreshCatalog
7140
+ ]);
7141
+ (0, react.useEffect)(() => {
7142
+ if (!visible || state.status !== "ready" || busy || refreshing) return;
7143
+ let stopped = false;
7144
+ let timer;
7145
+ const tick = async () => {
7146
+ try {
7147
+ const version = unwrap(await withDeadline(remote.catalogVersion(), POLL_SAMPLE_TIMEOUT_MS), "capabilityPolicy.catalogVersion");
7148
+ pollFailures.current = 0;
7149
+ if (versionChanged(seenVersion.current, version)) {
7150
+ seenVersion.current = version;
7151
+ await reload();
7152
+ }
7153
+ } catch {
7154
+ pollFailures.current += 1;
7155
+ }
7156
+ if (!stopped) timer = setTimeout(() => void tick(), pollDelayMs(pollFailures.current));
7157
+ };
7158
+ timer = setTimeout(() => void tick(), 0);
7159
+ return () => {
7160
+ stopped = true;
7161
+ if (timer !== void 0) clearTimeout(timer);
7162
+ };
7163
+ }, [
7164
+ visible,
7165
+ state.status,
7166
+ busy,
7167
+ refreshing,
7168
+ remote,
7169
+ reload
7170
+ ]);
6652
7171
  /** Move one or more capability ids to the next class in the click cycle. */
6653
7172
  const cycleClass = (0, react.useCallback)(async (ids, kind) => {
6654
7173
  if (ids.length === 0 || busy) return;
@@ -6729,9 +7248,15 @@ function CapabilitySection(props) {
6729
7248
  })
6730
7249
  ]
6731
7250
  }),
6732
- notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7251
+ notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6733
7252
  className: "mc-notice",
6734
- children: notice
7253
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice }), retryMcp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7254
+ type: "button",
7255
+ className: "mc-catalog-btn",
7256
+ disabled: refreshing,
7257
+ onClick: () => void refreshRegisteredMcp(retryMcp),
7258
+ children: t("retry")
7259
+ })]
6735
7260
  }),
6736
7261
  state.status === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadyBody, {
6737
7262
  remote,
@@ -6744,19 +7269,26 @@ function CapabilitySection(props) {
6744
7269
  onToggleServer: toggleServer,
6745
7270
  onCycle: cycleClass,
6746
7271
  onRefresh: () => void refreshCatalog(),
6747
- refreshing,
7272
+ onRefreshMcp: (serverName) => void refreshRegisteredMcp(serverName),
6748
7273
  onNotice: setNotice
6749
7274
  })
6750
7275
  ]
6751
7276
  });
6752
7277
  }
6753
7278
  function ReadyBody(props) {
6754
- const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh, refreshing, onNotice } = props;
6755
- const { servers, skills } = groupRows(snapshot.rows);
6756
- const projectSkills = skills.filter((skill) => PROJECT_SKILL_SOURCES.has(skill.source ?? ""));
6757
- const globalSkills = skills.filter((skill) => !PROJECT_SKILL_SOURCES.has(skill.source ?? ""));
7279
+ const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh, onRefreshMcp, onNotice } = props;
7280
+ /** Name filter, shared by both tabs: it narrows whichever list is shown. */
7281
+ const [filter, setFilter] = (0, react.useState)("");
7282
+ const nameFilter = compileNameFilter(filter);
7283
+ const needle = nameFilter.needle;
7284
+ const shownRows = filterRows(snapshot.rows, nameFilter, {
7285
+ builtIn: t("builtInGroup"),
7286
+ source: (source) => sourceLabel(source, t)
7287
+ });
7288
+ const { servers, skills } = groupRows(shownRows);
7289
+ const { presetSkills, presetGroups, projectSkills, globalSkills } = splitSkillGroups(skills);
6758
7290
  const [skillTab, setSkillTab] = (0, react.useState)("global");
6759
- const statRows = activeTab === "tools" ? snapshot.rows.filter((r) => r.kind === "tool") : skillTab === "project" ? projectSkills : globalSkills;
7291
+ const statRows = activeTab === "tools" ? shownRows.filter((r) => r.kind === "tool") : skillTab === "project" ? projectSkills : skillTab === "preset" ? presetSkills : globalSkills;
6760
7292
  const summary = CLASS_KEYS.map((cls) => ({
6761
7293
  cls,
6762
7294
  count: countByClass(statRows, cls)
@@ -6895,9 +7427,33 @@ function ReadyBody(props) {
6895
7427
  }
6896
7428
  }, [remote]);
6897
7429
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6898
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
6899
- className: "mc-heading",
6900
- children: t("title")
7430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7431
+ className: "mc-heading-row",
7432
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7433
+ className: "mc-heading-meta",
7434
+ children: [
7435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
7436
+ className: "mc-heading",
7437
+ children: t("title")
7438
+ }),
7439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7440
+ className: "mc-package-name",
7441
+ children: t("packageName")
7442
+ }),
7443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7444
+ className: "mc-version",
7445
+ title: PLUGIN_TITLE,
7446
+ children: ["v", PLUGIN_VERSION]
7447
+ })
7448
+ ]
7449
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7450
+ className: "mc-feedback-link",
7451
+ href: "https://github.com/PKUfudawei/dsh-capability-menu",
7452
+ target: "_blank",
7453
+ rel: "noopener noreferrer",
7454
+ "aria-label": t("feedback"),
7455
+ children: t("feedback")
7456
+ })]
6901
7457
  }),
6902
7458
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6903
7459
  className: "mc-desc-row",
@@ -6936,35 +7492,36 @@ function ReadyBody(props) {
6936
7492
  })]
6937
7493
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6938
7494
  className: "mc-summary",
6939
- children: [
6940
- summary.map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6941
- className: `mc-chip mc-chip--${cls}`,
6942
- children: [
6943
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6944
- className: `mc-dot mc-dot--${cls}`,
6945
- "aria-hidden": "true"
6946
- }),
6947
- t(CLASS_SHORT_KEYS[cls]),
6948
- " · ",
6949
- count
6950
- ]
6951
- }, cls)),
6952
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6953
- type: "button",
6954
- className: "mc-catalog-btn",
6955
- onClick: () => onRefresh(),
6956
- disabled: refreshing,
6957
- children: refreshing ? t("refreshing") : t("refresh")
6958
- }),
6959
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6960
- type: "button",
6961
- className: "mc-catalog-btn",
6962
- onClick: () => setRegisterOpen(true),
6963
- children: t("registerCapability")
6964
- })
6965
- ]
7495
+ children: [summary.map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7496
+ className: `mc-chip mc-chip--${cls}`,
7497
+ children: [
7498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7499
+ className: `mc-dot mc-dot--${cls}`,
7500
+ "aria-hidden": "true"
7501
+ }),
7502
+ t(CLASS_SHORT_KEYS[cls]),
7503
+ " · ",
7504
+ count
7505
+ ]
7506
+ }, cls)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7507
+ type: "button",
7508
+ className: "mc-catalog-btn",
7509
+ onClick: () => setRegisterOpen(true),
7510
+ children: t("registerCapability")
7511
+ })]
6966
7512
  })]
6967
7513
  }),
7514
+ snapshot.rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7515
+ className: "mc-filter",
7516
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7517
+ type: "search",
7518
+ value: filter,
7519
+ placeholder: t("filterByName"),
7520
+ "aria-label": t("filterByName"),
7521
+ title: t("filterHint"),
7522
+ onChange: (e) => setFilter(e.target.value)
7523
+ })
7524
+ }),
6968
7525
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6969
7526
  role: "tabpanel",
6970
7527
  hidden: activeTab !== "tools",
@@ -6973,7 +7530,7 @@ function ReadyBody(props) {
6973
7530
  className: "mc-panel-inner",
6974
7531
  children: servers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6975
7532
  className: "mc-empty",
6976
- children: t("emptyTools")
7533
+ children: needle === "" ? t("emptyTools") : t("filterNoMatch")
6977
7534
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: servers.map(({ server, tools }) => {
6978
7535
  const open = openServers.has(server);
6979
7536
  const counts = CLASS_KEYS.map((cls) => ({
@@ -6995,7 +7552,7 @@ function ReadyBody(props) {
6995
7552
  }
6996
7553
  },
6997
7554
  children: [
6998
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
7555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTriangleRightFill, {
6999
7556
  size: 12,
7000
7557
  className: `mc-chevron${open ? " mc-chevron--open" : ""}`
7001
7558
  }),
@@ -7090,35 +7647,47 @@ function ReadyBody(props) {
7090
7647
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7091
7648
  role: "tabpanel",
7092
7649
  hidden: activeTab !== "skills",
7093
- className: "mc-panel mc-panel--tight",
7650
+ className: `mc-panel${skills.length > 0 ? " mc-panel--tight" : ""}`,
7094
7651
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7095
7652
  className: "mc-panel-inner",
7096
7653
  children: skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7097
7654
  className: "mc-empty",
7098
- children: t("emptySkills")
7655
+ children: needle === "" ? t("emptySkills") : t("filterNoMatch")
7099
7656
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7100
7657
  className: "mc-subtabs",
7101
7658
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7102
7659
  className: "mc-tab-group",
7103
7660
  role: "tablist",
7104
7661
  "aria-label": t("skillsGroup"),
7105
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7106
- type: "button",
7107
- role: "tab",
7108
- className: "mc-tab",
7109
- "aria-selected": skillTab === "global",
7110
- "data-active": skillTab === "global" ? "true" : void 0,
7111
- onClick: () => setSkillTab("global"),
7112
- children: t("globalSkills")
7113
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7114
- type: "button",
7115
- role: "tab",
7116
- className: "mc-tab",
7117
- "aria-selected": skillTab === "project",
7118
- "data-active": skillTab === "project" ? "true" : void 0,
7119
- onClick: () => setSkillTab("project"),
7120
- children: t("projectSkills")
7121
- })]
7662
+ children: [
7663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7664
+ type: "button",
7665
+ role: "tab",
7666
+ className: "mc-tab",
7667
+ "aria-selected": skillTab === "global",
7668
+ "data-active": skillTab === "global" ? "true" : void 0,
7669
+ onClick: () => setSkillTab("global"),
7670
+ children: t("globalSkills")
7671
+ }),
7672
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7673
+ type: "button",
7674
+ role: "tab",
7675
+ className: "mc-tab",
7676
+ "aria-selected": skillTab === "project",
7677
+ "data-active": skillTab === "project" ? "true" : void 0,
7678
+ onClick: () => setSkillTab("project"),
7679
+ children: t("projectSkills")
7680
+ }),
7681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7682
+ type: "button",
7683
+ role: "tab",
7684
+ className: "mc-tab",
7685
+ "aria-selected": skillTab === "preset",
7686
+ "data-active": skillTab === "preset" ? "true" : void 0,
7687
+ onClick: () => setSkillTab("preset"),
7688
+ children: t("presetSkills")
7689
+ })
7690
+ ]
7122
7691
  })
7123
7692
  }), skillTab === "global" ? globalSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7124
7693
  skills: globalSkills,
@@ -7132,7 +7701,7 @@ function ReadyBody(props) {
7132
7701
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7133
7702
  className: "mc-empty",
7134
7703
  children: t("emptyGlobalSkills")
7135
- }) : projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7704
+ }) : skillTab === "project" ? projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7136
7705
  skills: projectSkills,
7137
7706
  remote,
7138
7707
  busy,
@@ -7144,6 +7713,24 @@ function ReadyBody(props) {
7144
7713
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7145
7714
  className: "mc-empty",
7146
7715
  children: t("emptyProjectSkills")
7716
+ }) : presetGroups.length > 0 ? presetGroups.map(([preset, group]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7717
+ className: "mc-skill-group",
7718
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7719
+ className: "mc-skill-group-title",
7720
+ children: preset
7721
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7722
+ skills: group,
7723
+ remote,
7724
+ busy,
7725
+ t,
7726
+ onCycle,
7727
+ editableSkills,
7728
+ onEditSkill: openSkillEdit,
7729
+ onAdoptSkill: adoptSkill
7730
+ })]
7731
+ }, preset)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7732
+ className: "mc-empty",
7733
+ children: t("emptyPresetSkills")
7147
7734
  })] })
7148
7735
  })
7149
7736
  }),
@@ -7174,16 +7761,24 @@ function ReadyBody(props) {
7174
7761
  className: "mc-preview-hint",
7175
7762
  children: toolDetail.message
7176
7763
  }),
7177
- toolDetail.status === "ready" && toolDetail.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
7764
+ toolDetail.status === "ready" && toolDetail.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7178
7765
  className: "mc-preview-body",
7179
- children: JSON.stringify({
7180
- type: "function",
7181
- function: {
7182
- name: toolDetail.detail.name,
7183
- description: toolDetail.detail.description,
7184
- parameters: toolDetail.detail.parameters
7185
- }
7186
- }, null, 2)
7766
+ children: [toolDetail.detail.description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7767
+ className: "mc-tool-description",
7768
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownPreview, {
7769
+ text: toolDetail.detail.description,
7770
+ t
7771
+ })
7772
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
7773
+ className: "mc-preview-plain",
7774
+ children: JSON.stringify({
7775
+ type: "function",
7776
+ function: {
7777
+ name: toolDetail.detail.name,
7778
+ parameters: toolDetail.detail.parameters
7779
+ }
7780
+ }, null, 2)
7781
+ })]
7187
7782
  })
7188
7783
  ]
7189
7784
  })
@@ -7199,8 +7794,9 @@ function ReadyBody(props) {
7199
7794
  setEditMcp(void 0);
7200
7795
  setEditSkill(void 0);
7201
7796
  },
7202
- onChanged: (notice) => {
7203
- onRefresh();
7797
+ onChanged: (notice, refreshMcpServer) => {
7798
+ if (refreshMcpServer !== void 0) onRefreshMcp(refreshMcpServer);
7799
+ else onRefresh();
7204
7800
  if (notice !== void 0) onNotice(notice);
7205
7801
  }
7206
7802
  }),
@@ -7405,7 +8001,7 @@ function SkillList(props) {
7405
8001
  }),
7406
8002
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7407
8003
  className: "mc-tree-icon",
7408
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
8004
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTriangleRightFill, {
7409
8005
  size: 10,
7410
8006
  className: `mc-chevron${open ? " mc-chevron--open" : ""}`
7411
8007
  })
@@ -7469,7 +8065,7 @@ function SkillList(props) {
7469
8065
  }
7470
8066
  },
7471
8067
  children: [
7472
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
8068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTriangleRightFill, {
7473
8069
  size: 12,
7474
8070
  className: `mc-chevron${open ? " mc-chevron--open" : ""}`
7475
8071
  }),
@@ -7498,7 +8094,11 @@ function SkillList(props) {
7498
8094
  "aria-hidden": "true"
7499
8095
  }), t(CLASS_SHORT_KEYS[skill.class])]
7500
8096
  }),
7501
- !editableSkills.has(skill.id) && (ADOPTABLE_SKILL_SOURCES.has(skill.source ?? "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8097
+ !editableSkills.has(skill.id) && (skill.preset !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8098
+ className: "mc-source",
8099
+ title: t("skillFromPresetHint"),
8100
+ children: t("skillFromPreset", { preset: skill.preset })
8101
+ }) : ADOPTABLE_SKILL_SOURCES.has(skill.source ?? "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7502
8102
  type: "button",
7503
8103
  className: "mc-count mc-count--action",
7504
8104
  disabled: busy,
@@ -7597,9 +8197,15 @@ function SkillList(props) {
7597
8197
  onClick: () => setPreview(null),
7598
8198
  children: t("previewClose")
7599
8199
  })]
7600
- }), preview.content !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8200
+ }), preview.content !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7601
8201
  className: "mc-preview-body",
7602
- children: preview.content
8202
+ children: /\.md$/i.test(preview.relPath) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownFilePreview, {
8203
+ text: preview.content,
8204
+ t
8205
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8206
+ className: "mc-preview-plain",
8207
+ children: preview.content
8208
+ })
7603
8209
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7604
8210
  className: "mc-preview-hint",
7605
8211
  children: preview.error ?? "…"
@@ -7626,6 +8232,8 @@ async function apply(ctx) {
7626
8232
  const zh = {
7627
8233
  nav: "能力菜单",
7628
8234
  title: "能力菜单",
8235
+ packageName: "capability-menu",
8236
+ feedback: "反馈 ↗",
7629
8237
  desc: "管理工具与技能的 常驻 / 按需 / 禁用 三档分类。",
7630
8238
  resident: "Resident(常驻上下文)",
7631
8239
  "on-demand": "On-demand(按需发现)",
@@ -7638,8 +8246,13 @@ async function apply(ctx) {
7638
8246
  builtInGroup: "系统内置",
7639
8247
  globalSkills: "全局技能",
7640
8248
  projectSkills: "项目技能",
8249
+ presetSkills: "预设技能",
7641
8250
  emptyGlobalSkills: "暂无全局技能",
7642
8251
  emptyProjectSkills: "暂无项目技能",
8252
+ emptyPresetSkills: "暂无预设技能",
8253
+ filterByName: "按名字、分组或正则过滤…",
8254
+ filterHint: "忽略大小写。匹配工具/技能名、MCP server、技能来源标签与预设 id;也可以直接写正则,例如 cordis|ptc 或 /^mcp__dbx/。",
8255
+ filterNoMatch: "没有匹配的能力(清空过滤框可恢复完整列表)",
7643
8256
  emptyTools: "暂无工具",
7644
8257
  emptySkills: "暂无 Skill",
7645
8258
  toolCount: "{count} 个工具",
@@ -7649,11 +8262,16 @@ async function apply(ctx) {
7649
8262
  cycleHint: "点击标签切换分类(按需 → 禁用 → 常驻)",
7650
8263
  notPreviewable: "该文件不是可预览的文本文件",
7651
8264
  previewClose: "关闭",
8265
+ markdownCode: "代码",
8266
+ markdownWrap: "自动换行",
8267
+ markdownUnwrap: "不换行",
8268
+ markdownCopy: "复制",
8269
+ markdownCopied: "已复制",
8270
+ markdownFootnotes: "脚注",
7652
8271
  detailNotFound: "未找到该工具的详情",
7653
8272
  cycleOverridden: "分类未生效:{count} 个能力被更高优先级规则覆盖(如通配规则),可移除对应通配规则后重试",
7654
- refresh: "刷新",
7655
- refreshing: "刷新中…",
7656
8273
  refreshFailed: "刷新失败,请查看日志",
8274
+ mcpToolsPending: "已注册「{name}」,但暂未加载出工具。请检查连接和认证后点刷新。",
7657
8275
  retry: "重试",
7658
8276
  carrierFailureHint: "请求未到达服务端:浏览器与 dsh 之间的连接已断开(Failed to fetch 属传输层失败,服务端方法未执行)。若刚重启过 dsh web,请先硬刷新页面(Ctrl/Cmd+Shift+R)再重试。",
7659
8277
  registerCapability: "注册能力",
@@ -7687,20 +8305,24 @@ async function apply(ctx) {
7687
8305
  skillAdopted: "已纳入管理:{path}",
7688
8306
  skillSource: "来源:{source}",
7689
8307
  skillSourceHint: "该技能由 dsh 从其它根发现,不在本插件管理的技能目录里,因此没有「编辑」。",
8308
+ skillFromPreset: "来自预设 {preset}",
8309
+ skillFromPresetHint: "该技能随 agent preset 分发,只对挂载了该预设的会话可见;不能纳入管理(那会把它软链进全局技能根,对所有会话生效)。",
8310
+ githubImporting: "正在从 GitHub 获取并校验 Skill,完成后会自动注册…",
8311
+ importing: "导入中…",
7690
8312
  skillUnmanaged: "未纳入管理",
7691
8313
  sourceCustom: "自定义技能目录",
7692
8314
  sourceBundled: "随 dsh 预置",
7693
8315
  adoptSkill: "纳入管理",
7694
8316
  adoptSkillHint: "把它软链进 dsh 的技能根目录(默认 ~/.dsh/skills),内容不动;之后就能编辑与移除了。",
7695
8317
  adoptSkillTitle: "纳入管理「{name}」?",
7696
- skillDirHint: "目录内需有 SKILL.md(按 dsh 的 skill 格式校验)。技能名以 SKILL.md 里声明的为准,目录名只决定注册后的软链名,两者不同也可以。",
8318
+ skillDirHint: "支持本机目录或公开 GitHub HTTPS 技能链接。GitHub 导入会校验 SKILL.md,并只保留对应技能目录;运行 dsh 的机器需安装 Git。技能名取自 SKILL.md。",
7697
8319
  serverName: "serverName",
7698
- serverNameImmutable: "serverName 不可修改:它构成工具名前缀 mcp__<serverName>__<tool>,并已被既有会话历史与权限规则引用;修改后这些记录将不再匹配。",
8320
+ serverNameImmutable: "名称用于工具 ID 和权限规则,注册后不可更改。",
7699
8321
  transport: "传输方式",
7700
8322
  transportStdio: "stdio(本地子进程)",
7701
8323
  transportHttp: "streamable-http(HTTP MCP 端点)",
7702
8324
  transportHintStdio: "由客户端将该 MCP server 作为子进程启动,JSON-RPC 报文经标准输入 / 输出交换。",
7703
- transportHintHttp: "该 MCP server 作为独立进程运行,可服务多个客户端连接;客户端经单个支持 POST / GET 的 HTTP 端点(MCP endpoint)与其通信。",
8325
+ transportHintHttp: "客户端通过支持 POST/GET 的 MCP 端点连接。",
7704
8326
  command: "命令",
7705
8327
  args: "参数(空格分隔)",
7706
8328
  cwd: "工作目录",
@@ -7720,6 +8342,8 @@ async function apply(ctx) {
7720
8342
  const en = {
7721
8343
  nav: "Capability Management",
7722
8344
  title: "Capability Management",
8345
+ packageName: "capability-menu",
8346
+ feedback: "Feedback ↗",
7723
8347
  desc: "Manage the Resident / On-demand / Disabled classification of tools and skills.",
7724
8348
  resident: "Resident",
7725
8349
  "on-demand": "On-demand",
@@ -7732,8 +8356,13 @@ async function apply(ctx) {
7732
8356
  builtInGroup: "System built-in",
7733
8357
  globalSkills: "Global skills",
7734
8358
  projectSkills: "Project skills",
8359
+ presetSkills: "Preset skills",
7735
8360
  emptyGlobalSkills: "No global skills",
7736
8361
  emptyProjectSkills: "No project skills",
8362
+ emptyPresetSkills: "No preset skills",
8363
+ filterByName: "Filter by name, group, or regex…",
8364
+ filterHint: "Case-insensitive. Matches tool/skill names, MCP servers, skill source labels and preset ids; a regular expression works too, e.g. cordis|ptc or /^mcp__dbx/.",
8365
+ filterNoMatch: "No capability matches (clear the filter to see the full list)",
7737
8366
  emptyTools: "No tools",
7738
8367
  emptySkills: "No skills",
7739
8368
  toolCount: "{count} tools",
@@ -7743,11 +8372,16 @@ async function apply(ctx) {
7743
8372
  cycleHint: "Click a tag to cycle its classification (On-demand → Disabled → Resident)",
7744
8373
  notPreviewable: "This file is not a previewable text file",
7745
8374
  previewClose: "Close",
8375
+ markdownCode: "Code",
8376
+ markdownWrap: "Wrap",
8377
+ markdownUnwrap: "Unwrap",
8378
+ markdownCopy: "Copy",
8379
+ markdownCopied: "Copied",
8380
+ markdownFootnotes: "Footnotes",
7746
8381
  detailNotFound: "Tool detail not found",
7747
8382
  cycleOverridden: "Classification not applied: {count} capability(ies) overridden by a higher-priority rule (e.g. a wildcard). Remove the matching wildcard rule and retry.",
7748
- refresh: "Refresh",
7749
- refreshing: "Refreshing…",
7750
8383
  refreshFailed: "Refresh failed; see the log",
8384
+ mcpToolsPending: "\"{name}\" is registered, but its tools have not loaded yet. Check its connection and authentication, then refresh.",
7751
8385
  retry: "Retry",
7752
8386
  carrierFailureHint: "The request never reached the server: the browser connection to dsh is broken (\"Failed to fetch\" is a transport failure, so no server method ran). If dsh web was just restarted, hard-refresh the page (Ctrl/Cmd+Shift+R) and retry.",
7753
8387
  registerCapability: "Register capability",
@@ -7781,20 +8415,24 @@ async function apply(ctx) {
7781
8415
  skillAdopted: "Adopted: {path}",
7782
8416
  skillSource: "Source: {source}",
7783
8417
  skillSourceHint: "dsh found this skill in another root, not in a skill directory this plugin manages, so there is no Edit button.",
8418
+ skillFromPreset: "From preset {preset}",
8419
+ skillFromPresetHint: "This skill ships with an agent preset and is visible only to sessions that mount it. It cannot be adopted (adopting would link it into the global skill root, making it apply to every session).",
8420
+ githubImporting: "Fetching and validating the Skill from GitHub… It will register automatically when complete.",
8421
+ importing: "Importing…",
7784
8422
  skillUnmanaged: "Not managed here",
7785
8423
  sourceCustom: "custom skill dirs",
7786
8424
  sourceBundled: "bundled with dsh",
7787
8425
  adoptSkill: "Adopt",
7788
8426
  adoptSkillHint: "Link it into dsh's skill root (default ~/.dsh/skills); the content is untouched, and it becomes editable and removable.",
7789
8427
  adoptSkillTitle: "Adopt \"{name}\"?",
7790
- skillDirHint: "The directory must hold a SKILL.md in dsh's skill format. The skill name is the one declared in SKILL.md; the directory name only names the link this registers, so the two may differ.",
8428
+ skillDirHint: "Enter a local directory or a public GitHub HTTPS skill URL. Imports validate SKILL.md and keep only the selected skill directory. Git must be installed on the machine running dsh. The skill name comes from SKILL.md.",
7791
8429
  serverName: "serverName",
7792
- serverNameImmutable: "serverName is immutable: it forms the tool name prefix mcp__<serverName>__<tool> and is referenced by existing session history and permission rules, which stop matching once it changes.",
8430
+ serverNameImmutable: "The name is locked after registration because tool IDs and permission rules use it.",
7793
8431
  transport: "Transport",
7794
8432
  transportStdio: "stdio (local subprocess)",
7795
8433
  transportHttp: "streamable-http (HTTP MCP endpoint)",
7796
8434
  transportHintStdio: "The client launches the MCP server as a subprocess and exchanges JSON-RPC messages over its standard input and output.",
7797
- transportHintHttp: "The server runs as an independent process that can handle multiple client connections; the client reaches it over a single HTTP endpoint (the MCP endpoint) supporting POST and GET.",
8435
+ transportHintHttp: "Connect through the MCP endpoint URL (supports POST and GET).",
7798
8436
  command: "Command",
7799
8437
  args: "Args (space separated)",
7800
8438
  cwd: "Working directory",
@@ -7824,6 +8462,7 @@ async function apply(ctx) {
7824
8462
  console.error("[capability-menu] $mount failed:", error);
7825
8463
  }
7826
8464
  const t = ctx.locale.bind(NS);
8465
+ installSettingsNavIcon(ctx, () => t("nav"));
7827
8466
  const remote = () => {
7828
8467
  try {
7829
8468
  return ctx.get("remote.capabilityPolicy");
@@ -7832,10 +8471,44 @@ async function apply(ctx) {
7832
8471
  return;
7833
8472
  }
7834
8473
  };
8474
+ /**
8475
+ * The two signals that say "the host catalog may have moved, and this page did
8476
+ * not ask": a settings document edit, and a carrier reconnect.
8477
+ *
8478
+ * `settings/document-updated` is one of the events dsh forwards to browsers
8479
+ * (so it arrives over the same validated channel as the remote calls) and it
8480
+ * fires for external edits too — composition files are edited outside the
8481
+ * browser, and nothing else on the wire announces that. `connection/reset` is
8482
+ * the client's own reconnect signal rather than a forwarded one, which is why
8483
+ * it is a plain local `ctx.on`; dsh's own settings sections listen to exactly
8484
+ * this pair, including the first connection, which closes the window where a
8485
+ * change lands between the initial read and the subscription.
8486
+ *
8487
+ * Neither subscription is fatal: the section falls back to its version poll.
8488
+ */
8489
+ const subscribeSignals = (listener) => {
8490
+ const disposers = [];
8491
+ const forwarded = ctx.remote;
8492
+ try {
8493
+ if (typeof forwarded.$on !== "function") throw new Error("ctx.remote.$on is unavailable");
8494
+ disposers.push(forwarded.$on("settings/document-updated", () => listener()));
8495
+ } catch (error) {
8496
+ console.error("[capability-menu] settings/document-updated subscription failed:", error);
8497
+ }
8498
+ try {
8499
+ disposers.push(ctx.on("connection/reset", () => listener()));
8500
+ } catch (error) {
8501
+ console.error("[capability-menu] connection/reset subscription failed:", error);
8502
+ }
8503
+ return () => {
8504
+ for (const dispose of disposers) dispose();
8505
+ };
8506
+ };
7835
8507
  const injected = () => {
7836
8508
  return {
7837
8509
  remote: remote(),
7838
8510
  t,
8511
+ subscribeSignals,
7839
8512
  ...mountError !== void 0 ? { mountError } : {}
7840
8513
  };
7841
8514
  };