@daweifu/capability-menu 0.1.2 → 0.1.4

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
@@ -284,7 +284,7 @@ function prefixIssues(path, issues) {
284
284
  return iss;
285
285
  });
286
286
  }
287
- function unwrapMessage(message) {
287
+ function unwrapMessage$1(message) {
288
288
  return typeof message === "string" ? message : message?.message;
289
289
  }
290
290
  function attachSchema(issues, start, inst) {
@@ -299,7 +299,7 @@ function finalizeIssue(iss, ctx, config) {
299
299
  else iss.schema = iss.inst;
300
300
  }
301
301
  const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;
302
- const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? "Invalid input";
302
+ const message = iss.message ? iss.message : unwrapMessage$1(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage$1(schemaError?.(iss)) ?? unwrapMessage$1(ctx?.error?.(iss)) ?? unwrapMessage$1(config.customError?.(iss)) ?? unwrapMessage$1(config.localeError?.(iss)) ?? "Invalid input";
303
303
  const { inst: _inst, schema: _schema, continue: _continue, input: _input, ...rest } = iss;
304
304
  rest.path ?? (rest.path = []);
305
305
  rest.message = message;
@@ -5190,12 +5190,57 @@ function superRefine(fn, params) {
5190
5190
  * namespace and exports a `TYPERT_REMOTE` contribution that the browser
5191
5191
  * plugin mounts via `ctx.remote.$mount(...)`.
5192
5192
  */
5193
+ const stringRecord$schema = record(string(), string());
5194
+ const mcpLocation$schema = object({
5195
+ id: string().readonly(),
5196
+ serverName: string().readonly(),
5197
+ transport: union([literal("stdio"), literal("streamable-http")]).readonly(),
5198
+ disabled: boolean().readonly(),
5199
+ command: string().optional().readonly(),
5200
+ args: array(string()).optional().readonly(),
5201
+ env: stringRecord$schema.optional().readonly(),
5202
+ cwd: string().optional().readonly(),
5203
+ url: string().optional().readonly(),
5204
+ headers: stringRecord$schema.optional().readonly(),
5205
+ toolCallTimeoutMs: number().optional().readonly()
5206
+ });
5207
+ const skillLocation$schema = object({
5208
+ name: string().readonly(),
5209
+ path: string().readonly(),
5210
+ linked: boolean().readonly(),
5211
+ valid: boolean().readonly(),
5212
+ root: union([literal("user"), literal("project")]).readonly(),
5213
+ entryDir: string().readonly(),
5214
+ target: string().optional().readonly()
5215
+ });
5216
+ const mcpInput$schema = object({
5217
+ serverName: string(),
5218
+ transport: union([literal("stdio"), literal("streamable-http")]),
5219
+ command: string().optional(),
5220
+ args: array(string()).optional(),
5221
+ env: stringRecord$schema.optional(),
5222
+ cwd: string().optional(),
5223
+ url: string().optional(),
5224
+ headers: stringRecord$schema.optional(),
5225
+ toolCallTimeoutMs: number().optional()
5226
+ });
5227
+ const mcpUpdateInput$schema = object({
5228
+ transport: union([literal("stdio"), literal("streamable-http")]),
5229
+ command: string().optional(),
5230
+ args: array(string()).optional(),
5231
+ env: stringRecord$schema.optional(),
5232
+ cwd: string().optional(),
5233
+ url: string().optional(),
5234
+ headers: stringRecord$schema.optional(),
5235
+ toolCallTimeoutMs: number().optional()
5236
+ });
5193
5237
  const capabilityRow$schema = object({
5194
5238
  id: string().readonly(),
5195
5239
  kind: union([literal("tool"), literal("skill")]).readonly(),
5196
5240
  name: string().readonly(),
5197
5241
  server: string().optional().readonly(),
5198
5242
  source: string().optional().readonly(),
5243
+ preset: string().optional().readonly(),
5199
5244
  class: union([
5200
5245
  literal("resident"),
5201
5246
  literal("on-demand"),
@@ -5229,7 +5274,8 @@ const toolDetail$schema = object({
5229
5274
  provider: string().readonly(),
5230
5275
  serverName: string().optional().readonly(),
5231
5276
  path: string().optional().readonly(),
5232
- source: string().optional().readonly()
5277
+ source: string().optional().readonly(),
5278
+ preset: string().optional().readonly()
5233
5279
  }).readonly(),
5234
5280
  tags: array(string()).readonly(),
5235
5281
  stats: object({
@@ -5257,7 +5303,7 @@ const TYPERT_REMOTE = {
5257
5303
  },
5258
5304
  sourceLocation: {
5259
5305
  file: "src/server/remote.ts",
5260
- line: 60,
5306
+ line: 61,
5261
5307
  column: 3
5262
5308
  }
5263
5309
  },
@@ -5284,7 +5330,7 @@ const TYPERT_REMOTE = {
5284
5330
  },
5285
5331
  sourceLocation: {
5286
5332
  file: "src/server/remote.ts",
5287
- line: 66,
5333
+ line: 67,
5288
5334
  column: 3
5289
5335
  }
5290
5336
  },
@@ -5302,7 +5348,25 @@ const TYPERT_REMOTE = {
5302
5348
  },
5303
5349
  sourceLocation: {
5304
5350
  file: "src/server/remote.ts",
5305
- line: 72,
5351
+ line: 73,
5352
+ column: 3
5353
+ }
5354
+ },
5355
+ {
5356
+ id: "@daweifu/capability-menu#capabilityPolicy/refresh",
5357
+ service: "capabilityPolicy",
5358
+ namespace: "capabilityPolicy",
5359
+ method: "refresh",
5360
+ invocation: { kind: "direct" },
5361
+ parameters: [],
5362
+ result: {
5363
+ mode: "strict",
5364
+ typeSymbol: "void",
5365
+ schema: _undefined()
5366
+ },
5367
+ sourceLocation: {
5368
+ file: "src/server/remote.ts",
5369
+ line: 84,
5306
5370
  column: 3
5307
5371
  }
5308
5372
  },
@@ -5339,7 +5403,7 @@ const TYPERT_REMOTE = {
5339
5403
  },
5340
5404
  sourceLocation: {
5341
5405
  file: "src/server/remote.ts",
5342
- line: 84,
5406
+ line: 85,
5343
5407
  column: 3
5344
5408
  }
5345
5409
  },
@@ -5375,7 +5439,7 @@ const TYPERT_REMOTE = {
5375
5439
  },
5376
5440
  sourceLocation: {
5377
5441
  file: "src/server/remote.ts",
5378
- line: 90,
5442
+ line: 91,
5379
5443
  column: 3
5380
5444
  }
5381
5445
  },
@@ -5402,7 +5466,7 @@ const TYPERT_REMOTE = {
5402
5466
  },
5403
5467
  sourceLocation: {
5404
5468
  file: "src/server/remote.ts",
5405
- line: 78,
5469
+ line: 79,
5406
5470
  column: 3
5407
5471
  }
5408
5472
  },
@@ -5420,29 +5484,1121 @@ const TYPERT_REMOTE = {
5420
5484
  },
5421
5485
  sourceLocation: {
5422
5486
  file: "src/server/remote.ts",
5423
- line: 100,
5487
+ line: 104,
5488
+ column: 3
5489
+ }
5490
+ },
5491
+ {
5492
+ id: "@daweifu/capability-menu#capabilityPolicy/listLocations",
5493
+ service: "capabilityPolicy",
5494
+ namespace: "capabilityPolicy",
5495
+ method: "listLocations",
5496
+ invocation: { kind: "direct" },
5497
+ parameters: [],
5498
+ result: {
5499
+ mode: "strict",
5500
+ typeSymbol: "@daweifu/capability-menu#McpLocation[]",
5501
+ schema: array(mcpLocation$schema)
5502
+ },
5503
+ sourceLocation: {
5504
+ file: "src/server/remote.ts",
5505
+ line: 193,
5506
+ column: 3
5507
+ }
5508
+ },
5509
+ {
5510
+ id: "@daweifu/capability-menu#capabilityPolicy/addLocation",
5511
+ service: "capabilityPolicy",
5512
+ namespace: "capabilityPolicy",
5513
+ method: "addLocation",
5514
+ invocation: { kind: "direct" },
5515
+ parameters: [{
5516
+ name: "input",
5517
+ wire: "input",
5518
+ source: "json",
5519
+ codec: {
5520
+ mode: "strict",
5521
+ typeSymbol: "@daweifu/capability-menu#McpInput",
5522
+ schema: mcpInput$schema
5523
+ }
5524
+ }],
5525
+ result: {
5526
+ mode: "strict",
5527
+ typeSymbol: "string",
5528
+ schema: string()
5529
+ },
5530
+ sourceLocation: {
5531
+ file: "src/server/remote.ts",
5532
+ line: 199,
5533
+ column: 3
5534
+ }
5535
+ },
5536
+ {
5537
+ id: "@daweifu/capability-menu#capabilityPolicy/removeLocation",
5538
+ service: "capabilityPolicy",
5539
+ namespace: "capabilityPolicy",
5540
+ method: "removeLocation",
5541
+ invocation: { kind: "direct" },
5542
+ parameters: [{
5543
+ name: "id",
5544
+ wire: "id",
5545
+ source: "json",
5546
+ codec: {
5547
+ mode: "strict",
5548
+ typeSymbol: "string",
5549
+ schema: string()
5550
+ }
5551
+ }],
5552
+ result: {
5553
+ mode: "strict",
5554
+ typeSymbol: "boolean",
5555
+ schema: boolean()
5556
+ },
5557
+ sourceLocation: {
5558
+ file: "src/server/remote.ts",
5559
+ line: 205,
5560
+ column: 3
5561
+ }
5562
+ },
5563
+ {
5564
+ id: "@daweifu/capability-menu#capabilityPolicy/updateLocation",
5565
+ service: "capabilityPolicy",
5566
+ namespace: "capabilityPolicy",
5567
+ method: "updateLocation",
5568
+ invocation: { kind: "direct" },
5569
+ parameters: [{
5570
+ name: "id",
5571
+ wire: "id",
5572
+ source: "json",
5573
+ codec: {
5574
+ mode: "strict",
5575
+ typeSymbol: "string",
5576
+ schema: string()
5577
+ }
5578
+ }, {
5579
+ name: "input",
5580
+ wire: "input",
5581
+ source: "json",
5582
+ codec: {
5583
+ mode: "strict",
5584
+ typeSymbol: "@daweifu/capability-menu#McpUpdateInput",
5585
+ schema: mcpUpdateInput$schema
5586
+ }
5587
+ }],
5588
+ result: {
5589
+ mode: "strict",
5590
+ typeSymbol: "boolean",
5591
+ schema: boolean()
5592
+ },
5593
+ sourceLocation: {
5594
+ file: "src/server/remote.ts",
5595
+ line: 211,
5596
+ column: 3
5597
+ }
5598
+ },
5599
+ {
5600
+ id: "@daweifu/capability-menu#capabilityPolicy/listSkillLocations",
5601
+ service: "capabilityPolicy",
5602
+ namespace: "capabilityPolicy",
5603
+ method: "listSkillLocations",
5604
+ invocation: { kind: "direct" },
5605
+ parameters: [],
5606
+ result: {
5607
+ mode: "strict",
5608
+ typeSymbol: "@daweifu/capability-menu#SkillLocation[]",
5609
+ schema: array(skillLocation$schema)
5610
+ },
5611
+ sourceLocation: {
5612
+ file: "src/server/remote.ts",
5613
+ line: 229,
5614
+ column: 3
5615
+ }
5616
+ },
5617
+ {
5618
+ id: "@daweifu/capability-menu#capabilityPolicy/addSkillLocation",
5619
+ service: "capabilityPolicy",
5620
+ namespace: "capabilityPolicy",
5621
+ method: "addSkillLocation",
5622
+ invocation: { kind: "direct" },
5623
+ parameters: [{
5624
+ name: "dir",
5625
+ wire: "dir",
5626
+ source: "json",
5627
+ codec: {
5628
+ mode: "strict",
5629
+ typeSymbol: "string",
5630
+ schema: string()
5631
+ }
5632
+ }, {
5633
+ name: "projectPath",
5634
+ wire: "projectPath",
5635
+ source: "json",
5636
+ acceptsUndefined: true,
5637
+ codec: {
5638
+ mode: "strict",
5639
+ typeSymbol: "string",
5640
+ schema: string().optional()
5641
+ }
5642
+ }],
5643
+ result: {
5644
+ mode: "strict",
5645
+ typeSymbol: "string",
5646
+ schema: string()
5647
+ },
5648
+ sourceLocation: {
5649
+ file: "src/server/remote.ts",
5650
+ line: 252,
5651
+ column: 3
5652
+ }
5653
+ },
5654
+ {
5655
+ id: "@daweifu/capability-menu#capabilityPolicy/removeSkillLocation",
5656
+ service: "capabilityPolicy",
5657
+ namespace: "capabilityPolicy",
5658
+ method: "removeSkillLocation",
5659
+ invocation: { kind: "direct" },
5660
+ parameters: [{
5661
+ name: "name",
5662
+ wire: "name",
5663
+ source: "json",
5664
+ codec: {
5665
+ mode: "strict",
5666
+ typeSymbol: "string",
5667
+ schema: string()
5668
+ }
5669
+ }, {
5670
+ name: "entryDir",
5671
+ wire: "entryDir",
5672
+ source: "json",
5673
+ acceptsUndefined: true,
5674
+ codec: {
5675
+ mode: "strict",
5676
+ typeSymbol: "string",
5677
+ schema: string().optional()
5678
+ }
5679
+ }],
5680
+ result: {
5681
+ mode: "strict",
5682
+ typeSymbol: "boolean",
5683
+ schema: boolean()
5684
+ },
5685
+ sourceLocation: {
5686
+ file: "src/server/remote.ts",
5687
+ line: 258,
5688
+ column: 3
5689
+ }
5690
+ },
5691
+ {
5692
+ id: "@daweifu/capability-menu#capabilityPolicy/adoptSkillLocation",
5693
+ service: "capabilityPolicy",
5694
+ namespace: "capabilityPolicy",
5695
+ method: "adoptSkillLocation",
5696
+ invocation: { kind: "direct" },
5697
+ parameters: [{
5698
+ name: "name",
5699
+ wire: "name",
5700
+ source: "json",
5701
+ codec: {
5702
+ mode: "strict",
5703
+ typeSymbol: "string",
5704
+ schema: string()
5705
+ }
5706
+ }],
5707
+ result: {
5708
+ mode: "strict",
5709
+ typeSymbol: "string",
5710
+ schema: string()
5711
+ },
5712
+ sourceLocation: {
5713
+ file: "src/server/remote.ts",
5714
+ line: 259,
5424
5715
  column: 3
5425
5716
  }
5717
+ },
5718
+ {
5719
+ id: "@daweifu/capability-menu#capabilityPolicy/updateSkillLocation",
5720
+ service: "capabilityPolicy",
5721
+ namespace: "capabilityPolicy",
5722
+ method: "updateSkillLocation",
5723
+ invocation: { kind: "direct" },
5724
+ parameters: [
5725
+ {
5726
+ name: "name",
5727
+ wire: "name",
5728
+ source: "json",
5729
+ codec: {
5730
+ mode: "strict",
5731
+ typeSymbol: "string",
5732
+ schema: string()
5733
+ }
5734
+ },
5735
+ {
5736
+ name: "dir",
5737
+ wire: "dir",
5738
+ source: "json",
5739
+ codec: {
5740
+ mode: "strict",
5741
+ typeSymbol: "string",
5742
+ schema: string()
5743
+ }
5744
+ },
5745
+ {
5746
+ name: "entryDir",
5747
+ wire: "entryDir",
5748
+ source: "json",
5749
+ acceptsUndefined: true,
5750
+ codec: {
5751
+ mode: "strict",
5752
+ typeSymbol: "string",
5753
+ schema: string().optional()
5754
+ }
5755
+ }
5756
+ ],
5757
+ result: {
5758
+ mode: "strict",
5759
+ typeSymbol: "boolean",
5760
+ schema: boolean()
5761
+ },
5762
+ sourceLocation: {
5763
+ file: "src/server/remote.ts",
5764
+ line: 264,
5765
+ column: 3
5766
+ }
5767
+ },
5768
+ {
5769
+ id: "@daweifu/capability-menu#capabilityPolicy/catalogVersion",
5770
+ service: "capabilityPolicy",
5771
+ namespace: "capabilityPolicy",
5772
+ method: "catalogVersion",
5773
+ invocation: { kind: "direct" },
5774
+ parameters: [],
5775
+ result: {
5776
+ mode: "strict",
5777
+ typeSymbol: "number",
5778
+ schema: number()
5779
+ },
5780
+ sourceLocation: {
5781
+ file: "src/server/remote.ts",
5782
+ line: 317,
5783
+ column: 3
5784
+ }
5785
+ }
5786
+ ]
5787
+ };
5788
+ //#endregion
5789
+ //#region src/client/store.ts
5790
+ /** Unwrap a RemoteResult-like, throwing a readable error on failure. */
5791
+ function unwrap(result, what) {
5792
+ if (!result.ok) throw new Error(`${what} failed: ${result.error.code}: ${result.error.message}`);
5793
+ return result.value;
5794
+ }
5795
+ /**
5796
+ * Unwrap a RemoteResult-like for a user-facing mutation.
5797
+ *
5798
+ * The policy service's messages are already written for the person at the
5799
+ * dialog (Chinese, naming the offending path), so the RPC name and error code
5800
+ * that `unwrap` adds for diagnostics are dropped here: they would surface as
5801
+ * `capabilityPolicy.addSkillLocation failed: <code>: 目录中没有 SKILL.md:…`
5802
+ * in a dialog that is otherwise plain Chinese.
5803
+ */
5804
+ function unwrapMessage(result) {
5805
+ if (!result.ok) throw new Error(result.error.message);
5806
+ return result.value;
5807
+ }
5808
+ /**
5809
+ * The last successful snapshot, at module scope.
5810
+ *
5811
+ * The section is remounted every time the settings tab is opened, and a cold
5812
+ * `classifyAll` can be slow once per page load (the plugin's mount awaits a full
5813
+ * catalog enumeration before the surface has data). Keeping the previous rows
5814
+ * lets a remount paint them immediately and revalidate behind the user instead
5815
+ * of showing a loading placeholder. This is a view cache only — every read is
5816
+ * still followed by an authoritative one.
5817
+ */
5818
+ let lastSnapshot;
5819
+ /** The previously loaded snapshot, when this page session has one. */
5820
+ function cachedSnapshot() {
5821
+ return lastSnapshot;
5822
+ }
5823
+ /** Load the classification list from the remote, remembering it for remounts. */
5824
+ async function loadSnapshot(remote) {
5825
+ lastSnapshot = { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
5826
+ return lastSnapshot;
5827
+ }
5828
+ //#endregion
5829
+ //#region src/client/LocationModal.tsx
5830
+ /**
5831
+ * 能力来源弹窗:注册与编辑 MCP 服务器 / Skill 目录。
5832
+ *
5833
+ * Two modes, one form implementation:
5834
+ *
5835
+ * - **注册** — opened from the header's 注册能力 button. Carries the two
5836
+ * sub-tabs (MCP servers / skill directories) and opens on the one matching
5837
+ * the main tab the user is on, so one button is context-sensitive. No list of
5838
+ * what is already registered: that lives on the entries themselves, behind
5839
+ * the per-row 编辑 buttons, where it is actually actionable.
5840
+ * - **编辑** — opened from a row's 编辑 button with the entry prefilled.
5841
+ *
5842
+ * Only 注册 / 保存 / 移除 are offered. Temporarily silencing a source is the
5843
+ * exposure policy's job (classify the capability as Disabled), so there is no
5844
+ * second enable switch here.
5845
+ */
5846
+ const CSS_ID$1 = "capability-menu-location-css";
5847
+ const CSS$1 = `
5848
+ .lm-block{display:flex;flex-direction:column;gap:8px}
5849
+ .lm-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}
5850
+ .lm-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
5851
+ .lm-btn--danger:hover{color:#b3261e;border-color:#b3261e}
5852
+ .lm-btn:disabled{opacity:.55;cursor:default}
5853
+ /* Each field is its own two-column grid with a fixed label track.
5854
+ A single form-wide grid looked tidier but was fragile: it needed
5855
+ .lm-field{display:contents}, which turned every hint into a free-floating grid
5856
+ item, and one extra half-width item pushed the following labels into column 2 and
5857
+ their inputs into column 1 — which is exactly how the streamable-http layout got
5858
+ scrambled (that branch has an odd number of items). Per-field grids mean a hint
5859
+ can only ever affect its own field, and a fixed label track (rather than
5860
+ min-width, which sizes per row) keeps every label column identical. */
5861
+ .lm-form{display:flex;flex-direction:column;gap:6px}
5862
+ .lm-field{--lm-label-w:104px;display:grid;grid-template-columns:var(--lm-label-w) 1fr;gap:2px 8px;align-items:start}
5863
+ .lm-label{grid-column:1;min-width:0;overflow-wrap:anywhere;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
5864
+ /* width:100% must come with border-box: the field carries a 1px border plus 8px of
5865
+ padding, which would otherwise overflow the column and clip. */
5866
+ .lm-input{grid-column:2;width:100%;box-sizing:border-box;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit;font-size:12px;line-height:20px;padding:2px 8px}
5867
+ .lm-input:disabled{opacity:.6}
5868
+ textarea.lm-input{resize:vertical;font-family:var(--dsw-font-markdown-code-block-font-family)}
5869
+ /* Lives inside its field, in the content column: it lines up with the box it
5870
+ annotates and takes the caption colour so it does not read as another label. */
5871
+ .lm-hint{grid-column:2;margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}
5872
+ .lm-error{margin:0;font-size:12px;color:#b3261e;word-break:break-word}
5873
+ .lm-actions{display:flex;gap:8px;justify-content:flex-end;padding:4px 0 0}
5874
+ /* Removal is irreversible, so it takes over the action row for one confirmation
5875
+ step: the message states the consequence, then the buttons act. */
5876
+ .lm-confirm{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border:1px solid #b3261e;border-radius:6px}
5877
+ .lm-confirm-msg{flex:1 1 auto;margin:0;font-size:12px;line-height:18px;word-break:break-word}
5878
+ .lm-confirm-actions{display:flex;gap:8px;flex-shrink:0}
5879
+ /* The modal shell itself carries no padding (it is shared with the read-only
5880
+ catalog viewer, whose body supplies its own); the form supplies it here. */
5881
+ .lm-body{padding:14px 16px;overflow:auto;flex:1 1 auto;min-height:0}
5882
+ `;
5883
+ if (typeof document !== "undefined" && document.querySelector(`style[data-css-id="${CSS_ID$1}"]`) === null) {
5884
+ const tag = document.createElement("style");
5885
+ tag.dataset.cssId = CSS_ID$1;
5886
+ tag.textContent = CSS$1;
5887
+ document.head.appendChild(tag);
5888
+ }
5889
+ /** Render a key/value map as `Key: Value` lines. */
5890
+ function toLines(record) {
5891
+ if (record === void 0) return "";
5892
+ return Object.entries(record).map(([key, value]) => `${key}: ${value}`).join("\n");
5893
+ }
5894
+ /** Parse `Key: Value` lines into a map; blank lines and lines without `:` are skipped. */
5895
+ function fromLines(text) {
5896
+ const out = {};
5897
+ for (const line of text.split("\n")) {
5898
+ const trimmed = line.trim();
5899
+ if (trimmed.length === 0) continue;
5900
+ const at = trimmed.indexOf(":");
5901
+ if (at <= 0) continue;
5902
+ const key = trimmed.slice(0, at).trim();
5903
+ const value = trimmed.slice(at + 1).trim();
5904
+ if (key.length > 0) out[key] = value;
5905
+ }
5906
+ return Object.keys(out).length > 0 ? out : void 0;
5907
+ }
5908
+ function LocationModal(props) {
5909
+ const { remote, t, defaultKind = "mcp", editMcp, editSkill, onClose, onChanged } = props;
5910
+ const editing = editMcp !== void 0 || editSkill !== void 0;
5911
+ const [subTab, setSubTab] = (0, react.useState)(editSkill !== void 0 ? "skill" : editMcp !== void 0 ? "mcp" : defaultKind);
5912
+ const [error, setError] = (0, react.useState)(null);
5913
+ const [busy, setBusy] = (0, react.useState)(false);
5914
+ /**
5915
+ * Which action is waiting for confirmation, if any. Two need one: 移除, and
5916
+ * 保存 on an entry that is a real directory — repointing unlinks the entry
5917
+ * first, which recursively deletes a real directory's contents.
5918
+ */
5919
+ const [confirm, setConfirm] = (0, react.useState)(null);
5920
+ const [serverName, setServerName] = (0, react.useState)(editMcp?.serverName ?? "");
5921
+ const [transport, setTransport] = (0, react.useState)(editMcp?.transport ?? "streamable-http");
5922
+ const [command, setCommand] = (0, react.useState)(editMcp?.command ?? "");
5923
+ const [args, setArgs] = (0, react.useState)((editMcp?.args ?? []).join(" "));
5924
+ const [cwd, setCwd] = (0, react.useState)(editMcp?.cwd ?? "");
5925
+ const [env, setEnv] = (0, react.useState)(toLines(editMcp?.env));
5926
+ const [url, setUrl] = (0, react.useState)(editMcp?.url ?? "");
5927
+ const [headers, setHeaders] = (0, react.useState)(toLines(editMcp?.headers));
5928
+ const [timeoutSec, setTimeoutSec] = (0, react.useState)(editMcp?.toolCallTimeoutMs === void 0 ? "" : String(Math.round(editMcp.toolCallTimeoutMs / 1e3)));
5929
+ const [skillDir, setSkillDir] = (0, react.useState)(editSkill?.target ?? editSkill?.path ?? "");
5930
+ const [skillRoot, setSkillRoot] = (0, react.useState)(editSkill?.root ?? "user");
5931
+ /** A path inside the project; the server derives the project root from it. */
5932
+ const [projectPath, setProjectPath] = (0, react.useState)("");
5933
+ /**
5934
+ * What the operator sees this entry called. They got here from a row named by
5935
+ * the skill's declared name, so the messages use that and not the entry's own
5936
+ * name — which is only ever handed back to the server, since that is what
5937
+ * addresses the symlink on disk.
5938
+ */
5939
+ const skillLabel = editSkill?.skillName ?? editSkill?.name ?? "";
5940
+ const title = (0, react.useMemo)(() => {
5941
+ if (editMcp !== void 0) return t("editMcp");
5942
+ if (editSkill !== void 0) return t("editSkillNamed", { name: skillLabel });
5943
+ return t("registerCapability");
5944
+ }, [
5945
+ editMcp,
5946
+ editSkill,
5947
+ skillLabel,
5948
+ t
5949
+ ]);
5950
+ /** Run a mutation; on success close and ask the parent to re-pull. */
5951
+ const submit = (0, react.useCallback)(async (action, noticeFor) => {
5952
+ if (busy) return;
5953
+ setBusy(true);
5954
+ try {
5955
+ const changed = await action();
5956
+ if (changed === false) throw new Error(t("entryNotFound"));
5957
+ setError(null);
5958
+ onChanged(noticeFor?.(changed));
5959
+ onClose();
5960
+ } catch (e) {
5961
+ setError(String(e));
5962
+ } finally {
5963
+ setBusy(false);
5964
+ }
5965
+ }, [
5966
+ busy,
5967
+ onChanged,
5968
+ onClose,
5969
+ t
5970
+ ]);
5971
+ const submitMcp = (0, react.useCallback)(() => {
5972
+ const trimmedTimeout = timeoutSec.trim();
5973
+ const timeoutMs = trimmedTimeout.length === 0 ? void 0 : Math.round(Number(trimmedTimeout) * 1e3);
5974
+ if (timeoutMs !== void 0 && !Number.isFinite(timeoutMs)) {
5975
+ setError(t("timeoutInvalid"));
5976
+ return;
5977
+ }
5978
+ const parsedEnv = fromLines(env);
5979
+ const parsedHeaders = fromLines(headers);
5980
+ const common = {
5981
+ transport,
5982
+ ...transport === "stdio" ? {
5983
+ command: command.trim(),
5984
+ ...args.trim().length > 0 ? { args: args.trim().split(/\s+/) } : {},
5985
+ ...parsedEnv !== void 0 ? { env: parsedEnv } : {},
5986
+ ...cwd.trim().length > 0 ? { cwd: cwd.trim() } : {}
5987
+ } : {
5988
+ url: url.trim(),
5989
+ ...parsedHeaders !== void 0 ? { headers: parsedHeaders } : {}
5990
+ },
5991
+ ...timeoutMs !== void 0 ? { toolCallTimeoutMs: timeoutMs } : {}
5992
+ };
5993
+ submit(async () => editMcp !== void 0 ? unwrapMessage(await remote.updateLocation(editMcp.id, common)) : unwrapMessage(await remote.addLocation({
5994
+ serverName: serverName.trim(),
5995
+ ...common
5996
+ })));
5997
+ }, [
5998
+ submit,
5999
+ remote,
6000
+ editMcp,
6001
+ serverName,
6002
+ transport,
6003
+ command,
6004
+ args,
6005
+ cwd,
6006
+ env,
6007
+ url,
6008
+ headers,
6009
+ timeoutSec,
6010
+ t
6011
+ ]);
6012
+ const submitSkill = (0, react.useCallback)(() => {
6013
+ const asked = skillRoot === "project" ? projectPath.trim() : void 0;
6014
+ 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) }));
6015
+ }, [
6016
+ submit,
6017
+ remote,
6018
+ editSkill,
6019
+ skillDir,
6020
+ skillRoot,
6021
+ projectPath,
6022
+ t
6023
+ ]);
6024
+ const remove = (0, react.useCallback)(() => {
6025
+ if (editMcp !== void 0) {
6026
+ submit(async () => unwrapMessage(await remote.removeLocation(editMcp.id)));
6027
+ return;
6028
+ }
6029
+ if (editSkill !== void 0) submit(async () => unwrapMessage(await remote.removeSkillLocation(editSkill.name, editSkill.entryDir)));
6030
+ }, [
6031
+ submit,
6032
+ remote,
6033
+ editMcp,
6034
+ editSkill
6035
+ ]);
6036
+ /**
6037
+ * What removal actually costs, which differs by target: an MCP row only loses
6038
+ * its config, a linked skill only loses its symlink — but `removeSkill`
6039
+ * recursively deletes a *real* directory in the skill root, so that case has to
6040
+ * say so before the click, not after.
6041
+ */
6042
+ const removalWarning = (0, react.useMemo)(() => {
6043
+ if (editMcp !== void 0) return t("confirmRemoveMcp", { name: editMcp.serverName });
6044
+ if (editSkill === void 0) return "";
6045
+ return editSkill.linked ? t("confirmRemoveSkillLink", { name: skillLabel }) : t("confirmRemoveSkillDir", { name: skillLabel });
6046
+ }, [
6047
+ editMcp,
6048
+ editSkill,
6049
+ skillLabel,
6050
+ t
6051
+ ]);
6052
+ /** Saving needs a confirmation only when it would delete a real directory. */
6053
+ const needsSaveConfirm = editSkill !== void 0 && !editSkill.linked;
6054
+ /** The action row, or the confirmation step that has replaced it. */
6055
+ const renderActions = (onSubmit) => confirm !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6056
+ className: "lm-confirm",
6057
+ role: "alert",
6058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6059
+ className: "lm-confirm-msg",
6060
+ children: confirm === "save" ? t("saveConfirmRealDir", { name: skillLabel }) : removalWarning
6061
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6062
+ className: "lm-confirm-actions",
6063
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6064
+ type: "button",
6065
+ className: "lm-btn",
6066
+ disabled: busy,
6067
+ onClick: () => setConfirm(null),
6068
+ children: t("cancel")
6069
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6070
+ type: "button",
6071
+ className: "lm-btn lm-btn--danger",
6072
+ disabled: busy,
6073
+ onClick: () => {
6074
+ if (confirm === "save") onSubmit();
6075
+ else remove();
6076
+ },
6077
+ children: confirm === "save" ? t("confirmSaveAnyway") : t("confirmRemove")
6078
+ })]
6079
+ })]
6080
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6081
+ className: "lm-actions",
6082
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6083
+ type: "button",
6084
+ className: "lm-btn",
6085
+ disabled: busy,
6086
+ onClick: () => {
6087
+ if (needsSaveConfirm) setConfirm("save");
6088
+ else onSubmit();
6089
+ },
6090
+ children: editing ? t("save") : t("register")
6091
+ }), editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6092
+ type: "button",
6093
+ className: "lm-btn lm-btn--danger",
6094
+ disabled: busy,
6095
+ onClick: () => setConfirm("remove"),
6096
+ children: t("remove")
6097
+ })]
6098
+ });
6099
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6100
+ className: "mc-preview-mask",
6101
+ onClick: onClose,
6102
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6103
+ className: "mc-preview",
6104
+ onClick: (e) => e.stopPropagation(),
6105
+ children: [
6106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6107
+ className: "mc-preview-head",
6108
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6109
+ className: "mc-preview-title",
6110
+ children: title
6111
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6112
+ type: "button",
6113
+ className: "mc-preview-close",
6114
+ onClick: onClose,
6115
+ children: t("previewClose")
6116
+ })]
6117
+ }),
6118
+ !editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6119
+ className: "mc-subtabs mc-catalog-tabs",
6120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6121
+ className: "mc-tab-group",
6122
+ role: "tablist",
6123
+ "aria-label": t("registerCapability"),
6124
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6125
+ type: "button",
6126
+ role: "tab",
6127
+ className: "mc-tab",
6128
+ "aria-selected": subTab === "mcp",
6129
+ "data-active": subTab === "mcp" ? "true" : void 0,
6130
+ onClick: () => setSubTab("mcp"),
6131
+ children: t("mcpServers")
6132
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6133
+ type: "button",
6134
+ role: "tab",
6135
+ className: "mc-tab",
6136
+ "aria-selected": subTab === "skill",
6137
+ "data-active": subTab === "skill" ? "true" : void 0,
6138
+ onClick: () => setSubTab("skill"),
6139
+ children: t("skillDirs")
6140
+ })]
6141
+ })
6142
+ }),
6143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6144
+ className: "lm-body",
6145
+ children: [error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6146
+ className: "lm-error",
6147
+ children: error
6148
+ }), subTab === "mcp" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6149
+ className: "lm-block",
6150
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6151
+ className: "lm-form",
6152
+ children: [
6153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6154
+ className: "lm-field",
6155
+ children: [
6156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6157
+ className: "lm-label",
6158
+ children: t("transport")
6159
+ }),
6160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6161
+ className: "lm-input",
6162
+ value: transport,
6163
+ onChange: (e) => setTransport(e.target.value === "stdio" ? "stdio" : "streamable-http"),
6164
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6165
+ value: "stdio",
6166
+ children: t("transportStdio")
6167
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6168
+ value: "streamable-http",
6169
+ children: t("transportHttp")
6170
+ })]
6171
+ }),
6172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6173
+ className: "lm-hint",
6174
+ children: transport === "stdio" ? t("transportHintStdio") : t("transportHintHttp")
6175
+ })
6176
+ ]
6177
+ }),
6178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6179
+ className: "lm-field",
6180
+ children: [
6181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6182
+ className: "lm-label",
6183
+ children: t("serverName")
6184
+ }),
6185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6186
+ className: "lm-input",
6187
+ value: serverName,
6188
+ disabled: editMcp !== void 0,
6189
+ onChange: (e) => setServerName(e.target.value)
6190
+ }),
6191
+ editMcp !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6192
+ className: "lm-hint",
6193
+ children: t("serverNameImmutable")
6194
+ })
6195
+ ]
6196
+ }),
6197
+ transport === "stdio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6199
+ className: "lm-field",
6200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6201
+ className: "lm-label",
6202
+ children: t("command")
6203
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6204
+ className: "lm-input",
6205
+ value: command,
6206
+ onChange: (e) => setCommand(e.target.value)
6207
+ })]
6208
+ }),
6209
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6210
+ className: "lm-field",
6211
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6212
+ className: "lm-label",
6213
+ children: t("args")
6214
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6215
+ className: "lm-input",
6216
+ value: args,
6217
+ onChange: (e) => setArgs(e.target.value)
6218
+ })]
6219
+ }),
6220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6221
+ className: "lm-field",
6222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6223
+ className: "lm-label",
6224
+ children: t("cwd")
6225
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6226
+ className: "lm-input",
6227
+ value: cwd,
6228
+ onChange: (e) => setCwd(e.target.value)
6229
+ })]
6230
+ }),
6231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6232
+ className: "lm-field",
6233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6234
+ className: "lm-label",
6235
+ children: t("env")
6236
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6237
+ className: "lm-input",
6238
+ rows: 3,
6239
+ value: env,
6240
+ onChange: (e) => setEnv(e.target.value)
6241
+ })]
6242
+ })
6243
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6244
+ className: "lm-field",
6245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6246
+ className: "lm-label",
6247
+ children: t("url")
6248
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6249
+ className: "lm-input",
6250
+ value: url,
6251
+ onChange: (e) => setUrl(e.target.value)
6252
+ })]
6253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6254
+ className: "lm-field",
6255
+ children: [
6256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6257
+ className: "lm-label",
6258
+ children: t("headers")
6259
+ }),
6260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6261
+ className: "lm-input",
6262
+ rows: 4,
6263
+ value: headers,
6264
+ onChange: (e) => setHeaders(e.target.value)
6265
+ }),
6266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6267
+ className: "lm-hint",
6268
+ children: t("headersHint")
6269
+ })
6270
+ ]
6271
+ })] }),
6272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6273
+ className: "lm-field",
6274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6275
+ className: "lm-label",
6276
+ children: t("timeout")
6277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6278
+ className: "lm-input",
6279
+ value: timeoutSec,
6280
+ placeholder: "60",
6281
+ onChange: (e) => setTimeoutSec(e.target.value)
6282
+ })]
6283
+ }),
6284
+ renderActions(() => void submitMcp())
6285
+ ]
6286
+ })
6287
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6288
+ className: "lm-block",
6289
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6290
+ className: "lm-form",
6291
+ children: [
6292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6293
+ className: "lm-field",
6294
+ children: [
6295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6296
+ className: "lm-label",
6297
+ children: t("skillRoot")
6298
+ }),
6299
+ editSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6300
+ className: "lm-input",
6301
+ value: t(editSkill.root === "project" ? "skillRootProject" : "skillRootUser"),
6302
+ disabled: true
6303
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6304
+ className: "lm-input",
6305
+ value: skillRoot,
6306
+ onChange: (e) => setSkillRoot(e.target.value === "project" ? "project" : "user"),
6307
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6308
+ value: "user",
6309
+ children: t("skillRootUser")
6310
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6311
+ value: "project",
6312
+ children: t("skillRootProject")
6313
+ })]
6314
+ }),
6315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6316
+ className: "lm-hint",
6317
+ children: skillRoot === "project" || editSkill?.root === "project" ? t("skillRootHintProject") : t("skillRootHintUser")
6318
+ })
6319
+ ]
6320
+ }),
6321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6322
+ className: "lm-field",
6323
+ children: [
6324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6325
+ className: "lm-label",
6326
+ children: t("skillDirPath")
6327
+ }),
6328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6329
+ className: "lm-input",
6330
+ value: skillDir,
6331
+ onChange: (e) => setSkillDir(e.target.value)
6332
+ }),
6333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6334
+ className: "lm-hint",
6335
+ children: t("skillDirHint")
6336
+ })
6337
+ ]
6338
+ }),
6339
+ editSkill === void 0 && skillRoot === "project" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6340
+ className: "lm-field",
6341
+ children: [
6342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6343
+ className: "lm-label",
6344
+ children: t("skillProjectPath")
6345
+ }),
6346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6347
+ className: "lm-input",
6348
+ value: projectPath,
6349
+ onChange: (e) => setProjectPath(e.target.value)
6350
+ }),
6351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6352
+ className: "lm-hint",
6353
+ children: t("skillProjectPathHint")
6354
+ })
6355
+ ]
6356
+ }),
6357
+ renderActions(() => void submitSkill())
6358
+ ]
6359
+ })
6360
+ })]
6361
+ })
6362
+ ]
6363
+ })
6364
+ });
6365
+ }
6366
+ /**
6367
+ * dsh's source labels for a project's own skill roots — the `project`-prefixed
6368
+ * providers of `@deepseek-ai/dsh-skill-filesystem`, which scan
6369
+ * `<projectRoot>/.dsh/skills` and `<projectRoot>/.agents/skills`. Every other
6370
+ * label (`user-dsh`, `user-agents`, `bundled`, `custom`, `runtime`) is a
6371
+ * user-level root. Shared so the server's location manager and the browser's
6372
+ * 全局 / 项目 grouping agree on what "project scoped" means.
6373
+ */
6374
+ const PROJECT_SKILL_SOURCES = /* @__PURE__ */ new Set(["project-dsh", "project-agents"]);
6375
+ /**
6376
+ * Source labels for user-level skill roots whose entries this plugin can adopt:
6377
+ * linking the skill's own directory into the user root (where the entry becomes
6378
+ * editable) without touching its content. `user-dsh` is excluded because that
6379
+ * root is already managed, and `bundled` because a packaged skill should not be
6380
+ * shadowed by a user copy.
6381
+ */
6382
+ const ADOPTABLE_SKILL_SOURCES = /* @__PURE__ */ new Set(["user-agents", "custom"]);
6383
+ //#endregion
6384
+ //#region src/client/skillGroups.ts
6385
+ /**
6386
+ * What the capability list decides about *which* rows it shows, kept out of
6387
+ * `CapabilitySection.tsx` as pure functions.
6388
+ *
6389
+ * The split is deliberate: the grouping rules are where this plugin got its one
6390
+ * real mis-grouping (preset skills filed under 全局技能), and they are also the
6391
+ * only part of the client a test can reach — this repo has no DOM harness, so
6392
+ * anything that lives inside the component can only be checked by hand.
6393
+ */
6394
+ /** Group tools by server name; skills stay flat (no server), both name-sorted. */
6395
+ function groupRows(rows) {
6396
+ const byServer = /* @__PURE__ */ new Map();
6397
+ const skills = [];
6398
+ for (const row of rows) {
6399
+ if (row.kind === "skill") {
6400
+ skills.push(row);
6401
+ continue;
6402
+ }
6403
+ const server = row.server ?? "built-in";
6404
+ const list = byServer.get(server);
6405
+ if (list === void 0) byServer.set(server, [row]);
6406
+ else list.push(row);
6407
+ }
6408
+ return {
6409
+ servers: [...byServer.entries()].map(([server, tools]) => ({
6410
+ server,
6411
+ tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
6412
+ })).sort((a, b) => a.server.localeCompare(b.server)),
6413
+ skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
6414
+ };
6415
+ }
6416
+ /**
6417
+ * Compile the box. A `/…/` wrapper is optional — it is stripped when present, so
6418
+ * both `cordis|ptc` and `/cordis|ptc/` are read as patterns — and it keeps the
6419
+ * common case of pasting a slash-delimited pattern from working.
6420
+ */
6421
+ function compileNameFilter(input) {
6422
+ const raw = input.trim();
6423
+ const needle = raw.toLowerCase();
6424
+ if (raw === "") return {
6425
+ needle,
6426
+ regex: false,
6427
+ test: () => true
6428
+ };
6429
+ const pattern = raw.length > 1 && raw.startsWith("/") && raw.endsWith("/") ? raw.slice(1, -1) : raw;
6430
+ try {
6431
+ const compiled = new RegExp(pattern, "i");
6432
+ return {
6433
+ needle,
6434
+ regex: true,
6435
+ test: (values) => values.some((v) => v !== void 0 && compiled.test(v))
6436
+ };
6437
+ } catch {
6438
+ return {
6439
+ needle,
6440
+ regex: false,
6441
+ test: (values) => values.some((v) => v !== void 0 && v.toLowerCase().includes(needle))
6442
+ };
6443
+ }
6444
+ }
6445
+ /**
6446
+ * Everything a row is matched against: its own name, plus the dimensions that
6447
+ * name the group it sits in — the MCP server (or the label the built-in group is
6448
+ * drawn with), the skill's source root, and the preset that ships it.
6449
+ *
6450
+ * Group headings are labels rather than row fields, which is exactly why typing
6451
+ * 系统 used to find nothing: the server id is `built-in`, and 系统内置 is only how
6452
+ * that id is painted. Both spellings are searchable now.
6453
+ */
6454
+ function searchableText(row, labels = {}) {
6455
+ const values = [row.name];
6456
+ if (row.server !== void 0) {
6457
+ values.push(row.server);
6458
+ if (row.server === "built-in") values.push(labels.builtIn);
6459
+ }
6460
+ if (row.source !== void 0) values.push(row.source, labels.source?.(row.source));
6461
+ values.push(row.preset);
6462
+ return values.filter((v) => v !== void 0 && v !== "");
6463
+ }
6464
+ /**
6465
+ * Narrow a list by the filter. Applied *before* grouping so a server header's
6466
+ * own counts describe the rows actually listed under it.
6467
+ */
6468
+ function filterRows(rows, filter, labels = {}) {
6469
+ if (filter.needle === "") return [...rows];
6470
+ return rows.filter((row) => filter.test(searchableText(row, labels)));
6471
+ }
6472
+ /**
6473
+ * Split skill rows across the three sub-tabs.
6474
+ *
6475
+ * Preset skills are split off by their `preset` field and never by `source`: a
6476
+ * preset's own `skill-filesystem` labels its skills `custom`, exactly like a
6477
+ * user-configured `customSkillDirs` entry, so a source-based rule drags both
6478
+ * into 全局技能 — filing a preset asset as global is precisely the mis-grouping
6479
+ * this split exists to fix. Rows keep the order they arrive in, so callers that
6480
+ * pass name-sorted rows get name-sorted groups.
6481
+ */
6482
+ function splitSkillGroups(skills) {
6483
+ const presetSkills = [];
6484
+ const projectSkills = [];
6485
+ const globalSkills = [];
6486
+ const presetGroups = [];
6487
+ for (const skill of skills) {
6488
+ const preset = skill.preset;
6489
+ if (preset !== void 0) {
6490
+ presetSkills.push(skill);
6491
+ const group = presetGroups.find(([id]) => id === preset);
6492
+ if (group === void 0) presetGroups.push([preset, [skill]]);
6493
+ else group[1].push(skill);
6494
+ continue;
5426
6495
  }
5427
- ]
5428
- };
6496
+ if (PROJECT_SKILL_SOURCES.has(skill.source ?? "")) projectSkills.push(skill);
6497
+ else globalSkills.push(skill);
6498
+ }
6499
+ presetGroups.sort(([a], [b]) => a.localeCompare(b));
6500
+ return {
6501
+ presetSkills,
6502
+ presetGroups,
6503
+ projectSkills,
6504
+ globalSkills
6505
+ };
6506
+ }
6507
+ /**
6508
+ * The sub-tab the Skills panel actually shows: the requested one, unless it is
6509
+ * 预设技能 while no preset ships skills. That tab does not exist then, and a
6510
+ * selection left on it must not survive — deployed presets are re-scanned on
6511
+ * every refresh, so the tab can disappear under the operator.
6512
+ */
6513
+ function resolveSkillTab(selected, groups) {
6514
+ return selected === "preset" && groups.presetSkills.length === 0 ? "global" : selected;
6515
+ }
6516
+ /** Rows in one tier. Counts the *displayed* rows, not the whole catalog. */
6517
+ function countByClass(rows, cls) {
6518
+ return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
6519
+ }
5429
6520
  //#endregion
5430
- //#region src/client/store.ts
5431
- /** Unwrap a RemoteResult-like, throwing a readable error on failure. */
5432
- function unwrap(result, what) {
5433
- if (!result.ok) throw new Error(`${what} failed: ${result.error.code}: ${result.error.message}`);
5434
- return result.value;
6521
+ //#region src/client/revalidate.ts
6522
+ /**
6523
+ * When to re-read the catalog, and how often to look — the decisions behind the
6524
+ * management page keeping itself fresh, kept as pure functions.
6525
+ *
6526
+ * Wiring lives in `CapabilitySection.tsx` (timers, the `visibilitychange`
6527
+ * listener, the two subscriptions); everything that could be got wrong lives
6528
+ * here, where a test can reach it. This repo has no DOM harness.
6529
+ */
6530
+ /**
6531
+ * How often a visible, idle page asks the host for its catalog version.
6532
+ *
6533
+ * This is the resolution of the one case nothing on the wire announces — a file
6534
+ * dropped into an already-registered skill directory — so it doubles as "how
6535
+ * long will I stare at a stale list after an agent changes something next to
6536
+ * me". 5s keeps that from being annoying; the cost is one tiny request per tick,
6537
+ * and only while this settings page is on screen.
6538
+ */
6539
+ const POLL_INTERVAL_MS = 5e3;
6540
+ /** Ceiling for the failure backoff, so a long outage does not spin. */
6541
+ const POLL_BACKOFF_MAX_MS = 6e4;
6542
+ /**
6543
+ * How long one version sample may take before it is treated as a failure.
6544
+ *
6545
+ * Without this, a request that neither resolves nor rejects would end the poll
6546
+ * for good: the next tick is only scheduled after the current one settles, and
6547
+ * nothing else restarts the effect while the page stays visible and idle.
6548
+ */
6549
+ const POLL_SAMPLE_TIMEOUT_MS = 15e3;
6550
+ /**
6551
+ * Delay before the next version poll.
6552
+ *
6553
+ * `failures` is the run of consecutive failures ending at this decision: 0
6554
+ * means the last sample succeeded and the plain interval applies, otherwise the
6555
+ * delay doubles until it hits the ceiling. This is what turns "the carrier
6556
+ * died" from a hot loop into a slow retry that heals itself when it comes back.
6557
+ */
6558
+ function pollDelayMs(failures) {
6559
+ if (!Number.isFinite(failures) || failures <= 0) return POLL_INTERVAL_MS;
6560
+ return Math.min(POLL_INTERVAL_MS * 2 ** failures, POLL_BACKOFF_MAX_MS);
5435
6561
  }
5436
- /** Load the classification list from the remote. */
5437
- async function loadSnapshot(remote) {
5438
- return { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
6562
+ /**
6563
+ * Whether a version sample means the catalog moved under the page.
6564
+ *
6565
+ * Deliberately `!==` rather than `>`: the counter is in-process, so it restarts
6566
+ * from zero when the plugin reloads, and a plain "greater than" test would sit
6567
+ * there believing a fresh host was the same host. `undefined` (nothing sampled
6568
+ * yet) only records — the page has just read the snapshot, and re-reading it
6569
+ * because it has no baseline would be busywork.
6570
+ */
6571
+ function versionChanged(seen, current) {
6572
+ return seen !== void 0 && seen !== current;
6573
+ }
6574
+ /**
6575
+ * The value of `work`, or a rejection if it has not settled within `ms`.
6576
+ *
6577
+ * Used so a carrier that accepts a request and then goes quiet degrades into
6578
+ * the same retry path as a carrier that refuses one.
6579
+ */
6580
+ function withDeadline(work, ms) {
6581
+ return new Promise((resolve, reject) => {
6582
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${ms}ms`)), ms);
6583
+ work.then((value) => {
6584
+ clearTimeout(timer);
6585
+ resolve(value);
6586
+ }, (error) => {
6587
+ clearTimeout(timer);
6588
+ reject(error);
6589
+ });
6590
+ });
6591
+ }
6592
+ /** Whether a signal may act now, or has to be parked until the page is idle. */
6593
+ function canRevalidate(gate) {
6594
+ return gate.visible && gate.ready && !gate.busy && !gate.rebuilding;
5439
6595
  }
5440
6596
  //#endregion
5441
6597
  //#region src/client/CapabilitySection.tsx
5442
6598
  /**
5443
6599
  * ⚠️ VERIFIED AGAINST REAL rc.8 CLIENT API.
5444
6600
  *
5445
- * React section for the 能力管理 settings tab. Follows the real dsh client
6601
+ * React section for the 能力菜单 settings tab. Follows the real dsh client
5446
6602
  * pattern (see `dsh-client-ui-settings-plugins`): two tabs (工具 / Skills)
5447
6603
  * under one heading, each listing capabilities with a clickable class chip.
5448
6604
  *
@@ -5482,9 +6638,15 @@ const CSS = `
5482
6638
  .mc-section{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary)}
5483
6639
  .mc-heading{margin:0;font-size:18px;font-weight:600}
5484
6640
  .mc-desc{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px}
6641
+ /* 说明行右侧放只读文档入口:把它从头部的计数行挪出来,计数多(Tools 常驻 ·
6642
+ 167)时那一行不再因此折行。 */
6643
+ .mc-desc-row{display:flex;align-items:center;justify-content:space-between;gap:12px}
6644
+ .mc-desc-row .mc-desc{flex:1 1 auto;min-width:0}
5485
6645
  .mc-summary{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end;align-items:center;padding-bottom:8px}
5486
6646
  .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}
5487
6647
  .mc-catalog-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
6648
+ /* 忙碌态靠变淡表示,而不是换文案:换文案会改按钮宽度,把这行挤到第二行。 */
6649
+ .mc-catalog-btn:disabled{cursor:default;opacity:.6}
5488
6650
  .mc-catalog-tabs{padding:8px 16px 0}
5489
6651
  .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}
5490
6652
  .mc-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:20px;white-space:nowrap}
@@ -5508,6 +6670,15 @@ body[data-ds-dark-theme] .mc-chip--resident{color:#96b6d1}
5508
6670
  body[data-ds-dark-theme] .mc-chip--on-demand{color:#d4b26b}
5509
6671
  body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
5510
6672
  .mc-tabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;justify-content:space-between;gap:22px}
6673
+ /* 名字过滤框:两个 tab 共用一条,宽度占满,与下方列表左对齐。
6674
+ 它属于上面的主 tab 栏,不属于下面的列表,所以两侧都把 .mc-section 的 12px
6675
+ 栏距收掉一部分:下划线—6px—输入框—8px—全局/项目 子页签文字(后者里的 4px
6676
+ 是子页签自己的 padding-top)。改之前是 12+12=24px 和 12+4=16px,读起来像
6677
+ 两个不相干的块。 */
6678
+ .mc-filter{display:flex;padding-top:0;margin-top:-6px;margin-bottom:-8px}
6679
+ .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}
6680
+ .mc-filter input:hover{border-color:var(--dsw-alias-border-l3)}
6681
+ .mc-filter input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-color:transparent}
5511
6682
  /* Skills sub-tab bar (全局技能/项目技能): same underline chrome, no right-side summary. */
5512
6683
  .mc-subtabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px}
5513
6684
  .mc-tab-group{display:flex;align-items:flex-end;gap:22px}
@@ -5515,7 +6686,19 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
5515
6686
  .mc-tab:hover,.mc-tab[data-active=true]{color:var(--dsw-alias-label-primary)}
5516
6687
  .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}
5517
6688
  .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}
5518
- .mc-panel{min-width:0;padding-top:12px}
6689
+ /* 面板的第一行必须和另一个 tab 的第一行从过滤框下沿出发走同样远,否则切 tab 时
6690
+ 首行会跳一下。这里留 4px,加上 .mc-section 的 4px 栏距(过滤框那侧被 -8px 收
6691
+ 掉后剩 4px)= 8px,正好是 Skills 面板子页签文字的位置(4px 栏距 + 子页签自己
6692
+ 的 4px padding-top)。原来这里是 12px,Tools 的首个分组因此落在过滤框下 16px
6693
+ 处,比「全局技能」那行低一半。 */
6694
+ .mc-panel{min-width:0;padding-top:4px}
6695
+ /* Skills panel: its first row is the 全局技能/项目技能 sub-tab bar, and that bar
6696
+ belongs to the header stack above it (rule → name filter → sub-tabs) rather
6697
+ than to the list below. So the panel adds no padding of its own, and the
6698
+ sub-tab labels keep a 4px top padding instead of the 7px they share with the
6699
+ primary tabs. */
6700
+ .mc-panel--tight{padding-top:0}
6701
+ .mc-panel--tight .mc-subtabs .mc-tab{padding-top:4px}
5519
6702
  .mc-panel-inner{display:flex;flex-direction:column;gap:14px}
5520
6703
  .mc-group{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}
5521
6704
  .mc-group-header{box-sizing:border-box;display:flex;align-items:center;gap:10px;width:100%;min-width:0;padding:10px 12px;background:var(--dsw-alias-bg-layer-1);border:0;color:inherit;font:inherit;text-align:left;cursor:pointer}
@@ -5536,6 +6719,11 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
5536
6719
  body[data-ds-dark-theme] .mc-count--resident{color:#96b6d1}
5537
6720
  body[data-ds-dark-theme] .mc-count--on-demand{color:#d4b26b}
5538
6721
  body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
6722
+ /* 「编辑」是动作,不是分类计数:沿用 chip 的尺寸以对齐,但用次要文字色、hover
6723
+ 才变蓝加下划线。裸用 .mc-count 时会继承 .mc-section 的 label-primary(浅色
6724
+ 主题下近黑),夹在着色的计数 chip 旁会读成一枚没有圆点的黑色 chip。 */
6725
+ .mc-count--action{color:var(--dsw-alias-label-secondary)}
6726
+ .mc-count--action:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}
5539
6727
  .mc-tools{border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base)}
5540
6728
  .mc-tool{display:flex;align-items:center;gap:10px;padding:7px 12px 7px 26px;font-size:13px;line-height:20px;cursor:pointer}
5541
6729
  .mc-tool:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -5547,14 +6735,30 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
5547
6735
  .mc-tag{font-size:11px;line-height:18px;padding:0 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}
5548
6736
  .mc-empty{padding:16px;text-align:center;font-size:13px;color:var(--dsw-alias-label-tertiary);border:1px dashed var(--dsw-alias-border-l2);border-radius:8px}
5549
6737
  .mc-error{padding:12px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;color:var(--dsw-alias-state-error-primary);font-size:13px}
5550
- .mc-error pre{margin:8px 0 0;white-space:pre-wrap;word-break:break-all;font-size:11px;color:var(--dsw-alias-label-tertiary)}
6738
+ .mc-error p{margin:0}
6739
+ .mc-error p.mc-error-hint{margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
6740
+ .mc-error-actions{display:flex;justify-content:flex-end;margin-top:10px}
5551
6741
  .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}
5552
6742
  .mc-skill{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
5553
6743
  .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}
5554
6744
  .mc-skill-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
5555
6745
  .mc-skill-name{font-weight:600;font-size:14px;line-height:20px;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
5556
6746
  .mc-skill-meta{margin-left:auto;display:flex;align-items:center;gap:8px;flex:0 1 auto;min-width:0}
6747
+ /* A row has no room for prose: let a long source label truncate instead of
6748
+ pushing the row's buttons past the right edge. */
6749
+ .mc-source{font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:0 1 auto}
6750
+ /* One sentence to confirm a row action: the shared modal chrome, sized down. */
6751
+ .mc-confirm-dialog{width:min(420px,calc(100vw - 48px))}
6752
+ .mc-confirm-body{display:flex;flex-direction:column;gap:12px;padding:14px 16px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
6753
+ .mc-confirm-body p{margin:0}
6754
+ /* A directory has no spaces to break at, and the modal is where it can show in
6755
+ full: wrap it instead of truncating it the way a row's 来源 slot must. */
6756
+ .mc-confirm-path{font-family:var(--dsw-font-markdown-code-block-font-family);word-break:break-all}
6757
+ .mc-confirm-actions{display:flex;justify-content:flex-end;gap:8px}
5557
6758
  .mc-skill-body{border-top:1px solid var(--dsw-alias-border-l1);padding:8px 12px 12px}
6759
+ /* 预设技能组:预设 id 是页签之下的分组小标题,不是新一级页签。 */
6760
+ .mc-skill-group{display:flex;flex-direction:column;gap:8px;min-width:0}
6761
+ .mc-skill-group-title{font-size:12px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-secondary);padding:0 2px}
5558
6762
  .mc-tree{display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:20px}
5559
6763
  .mc-tree-row{display:flex;align-items:center;gap:8px;padding:3px 4px;border-radius:6px;cursor:pointer;min-width:0}
5560
6764
  .mc-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -5577,37 +6781,54 @@ if (typeof document !== "undefined" && document.querySelector(`style[data-css-id
5577
6781
  tag.textContent = CSS;
5578
6782
  document.head.appendChild(tag);
5579
6783
  }
5580
- /** The reserved server key grouping harness-native (non-MCP) tools. */
5581
- const BUILT_IN_SERVER = "built-in";
5582
- /** Skills whose source root lives inside the current project. */
5583
- const PROJECT_SOURCES = /* @__PURE__ */ new Set(["project-dsh", "project-agents"]);
5584
- function groupRows(rows) {
5585
- const byServer = /* @__PURE__ */ new Map();
5586
- const skills = [];
5587
- for (const row of rows) {
5588
- if (row.kind === "skill") {
5589
- skills.push(row);
5590
- continue;
5591
- }
5592
- const server = row.server ?? BUILT_IN_SERVER;
5593
- const list = byServer.get(server);
5594
- if (list === void 0) byServer.set(server, [row]);
5595
- else list.push(row);
5596
- }
5597
- return {
5598
- servers: [...byServer.entries()].map(([server, tools]) => ({
5599
- server,
5600
- tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
5601
- })).sort((a, b) => a.server.localeCompare(b.server)),
5602
- skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
5603
- };
6784
+ /**
6785
+ * dsh's skill source labels, shown on rows this plugin cannot edit — so "why is
6786
+ * there no 编辑 button" has a visible answer. Labels that name one directory are
6787
+ * shown as that path; the rest name a category and go through the dictionary, so
6788
+ * none of dsh's internal labels (`custom`, `bundled`, …) reach the operator raw.
6789
+ */
6790
+ const SOURCE_PATHS = {
6791
+ "user-dsh": "~/.dsh/skills",
6792
+ "user-agents": "~/.agents/skills",
6793
+ "project-dsh": "<project>/.dsh/skills",
6794
+ "project-agents": "<project>/.agents/skills"
6795
+ };
6796
+ /** Source labels that name no single directory; shown via `t`. */
6797
+ const SOURCE_LABEL_KEYS = {
6798
+ "custom": "sourceCustom",
6799
+ "bundled": "sourceBundled"
6800
+ };
6801
+ /** How to name a skill's source root on a row we cannot manage. */
6802
+ function sourceLabel(source, t) {
6803
+ if (source === void 0) return t("skillUnmanaged");
6804
+ const path = SOURCE_PATHS[source];
6805
+ if (path !== void 0) return path;
6806
+ const key = SOURCE_LABEL_KEYS[source];
6807
+ return key === void 0 ? source : t(key);
5604
6808
  }
5605
- function countByClass(rows, cls) {
5606
- return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
6809
+ /**
6810
+ * Whether the document is visible. A hidden settings tab does no polling and
6811
+ * takes no re-reads; coming back is itself a revalidation trigger.
6812
+ */
6813
+ function useDocumentVisible() {
6814
+ const [visible, setVisible] = (0, react.useState)(true);
6815
+ (0, react.useEffect)(() => {
6816
+ const update = () => setVisible(document.visibilityState !== "hidden");
6817
+ update();
6818
+ document.addEventListener("visibilitychange", update);
6819
+ return () => document.removeEventListener("visibilitychange", update);
6820
+ }, []);
6821
+ return visible;
5607
6822
  }
5608
6823
  function CapabilitySection(props) {
5609
- const { remote, t, mountError, remoteKeys } = props;
5610
- const [state, setState] = (0, react.useState)({ status: "loading" });
6824
+ const { remote, t, mountError, subscribeSignals } = props;
6825
+ const [state, setState] = (0, react.useState)(() => {
6826
+ const cached = cachedSnapshot();
6827
+ return cached === void 0 ? { status: "loading" } : {
6828
+ status: "ready",
6829
+ snapshot: cached
6830
+ };
6831
+ });
5611
6832
  const [busy, setBusy] = (0, react.useState)(false);
5612
6833
  const [notice, setNotice] = (0, react.useState)(null);
5613
6834
  const [openServers, setOpenServers] = (0, react.useState)(/* @__PURE__ */ new Set());
@@ -5626,15 +6847,41 @@ function CapabilitySection(props) {
5626
6847
  snapshot
5627
6848
  })));
5628
6849
  } catch (e) {
5629
- setState({
6850
+ setState((prev) => prev.status === "ready" ? prev : {
5630
6851
  status: "error",
5631
6852
  message: String(e)
5632
6853
  });
6854
+ setNotice(String(e));
5633
6855
  }
5634
6856
  }, [remote, mountError]);
5635
6857
  (0, react.useEffect)(() => {
5636
6858
  reload();
5637
6859
  }, [reload]);
6860
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
6861
+ /**
6862
+ * Rebuild the host catalog, then re-read the classification list. Used after
6863
+ * registering a new source so the list reflects it without waiting for the
6864
+ * scheduler's debounce window.
6865
+ */
6866
+ const refreshCatalog = (0, react.useCallback)(async () => {
6867
+ if (refreshing) return;
6868
+ setRefreshing(true);
6869
+ try {
6870
+ if (remote !== void 0) unwrap(await remote.refresh(), "capabilityPolicy.refresh");
6871
+ await reload();
6872
+ setNotice(null);
6873
+ } catch (e) {
6874
+ console.error("[capability-menu] refresh failed:", e);
6875
+ setNotice(t("refreshFailed"));
6876
+ } finally {
6877
+ setRefreshing(false);
6878
+ }
6879
+ }, [
6880
+ refreshing,
6881
+ remote,
6882
+ reload,
6883
+ t
6884
+ ]);
5638
6885
  const toggleServer = (0, react.useCallback)((server) => {
5639
6886
  setOpenServers((prev) => {
5640
6887
  const next = new Set(prev);
@@ -5643,10 +6890,99 @@ function CapabilitySection(props) {
5643
6890
  return next;
5644
6891
  });
5645
6892
  }, []);
6893
+ const visible = useDocumentVisible();
6894
+ const [pendingSignal, setPendingSignal] = (0, react.useState)(false);
6895
+ const seenVersion = (0, react.useRef)(void 0);
6896
+ const pollFailures = (0, react.useRef)(0);
6897
+ const gate = (0, react.useRef)({
6898
+ visible: true,
6899
+ ready: false,
6900
+ busy: false,
6901
+ rebuilding: false
6902
+ });
6903
+ (0, react.useEffect)(() => {
6904
+ gate.current = {
6905
+ visible,
6906
+ ready: state.status === "ready",
6907
+ busy,
6908
+ rebuilding: refreshing
6909
+ };
6910
+ });
6911
+ /**
6912
+ * A signal that the host catalog may have moved. Runs now when the page is
6913
+ * idle; otherwise parks until it is, so a signal that lands mid-click is
6914
+ * never dropped (and never repaints the row under the cursor).
6915
+ */
6916
+ const onExternalSignal = (0, react.useCallback)(() => {
6917
+ if (canRevalidate(gate.current)) refreshCatalog();
6918
+ else setPendingSignal(true);
6919
+ }, [refreshCatalog]);
6920
+ (0, react.useEffect)(() => {
6921
+ return subscribeSignals?.(onExternalSignal);
6922
+ }, [subscribeSignals, onExternalSignal]);
6923
+ (0, react.useEffect)(() => {
6924
+ if (!pendingSignal) return;
6925
+ if (!canRevalidate({
6926
+ visible,
6927
+ ready: state.status === "ready",
6928
+ busy,
6929
+ rebuilding: refreshing
6930
+ })) return;
6931
+ setPendingSignal(false);
6932
+ refreshCatalog();
6933
+ }, [
6934
+ pendingSignal,
6935
+ visible,
6936
+ state.status,
6937
+ busy,
6938
+ refreshing,
6939
+ refreshCatalog
6940
+ ]);
6941
+ (0, react.useEffect)(() => {
6942
+ if (!visible || state.status !== "ready" || busy || refreshing) return;
6943
+ let stopped = false;
6944
+ let timer;
6945
+ const tick = async () => {
6946
+ try {
6947
+ const version = unwrap(await withDeadline(remote.catalogVersion(), POLL_SAMPLE_TIMEOUT_MS), "capabilityPolicy.catalogVersion");
6948
+ pollFailures.current = 0;
6949
+ if (versionChanged(seenVersion.current, version)) {
6950
+ seenVersion.current = version;
6951
+ await reload();
6952
+ }
6953
+ } catch {
6954
+ pollFailures.current += 1;
6955
+ }
6956
+ if (!stopped) timer = setTimeout(() => void tick(), pollDelayMs(pollFailures.current));
6957
+ };
6958
+ timer = setTimeout(() => void tick(), 0);
6959
+ return () => {
6960
+ stopped = true;
6961
+ if (timer !== void 0) clearTimeout(timer);
6962
+ };
6963
+ }, [
6964
+ visible,
6965
+ state.status,
6966
+ busy,
6967
+ refreshing,
6968
+ remote,
6969
+ reload
6970
+ ]);
5646
6971
  /** Move one or more capability ids to the next class in the click cycle. */
5647
6972
  const cycleClass = (0, react.useCallback)(async (ids, kind) => {
5648
6973
  if (ids.length === 0 || busy) return;
5649
6974
  setBusy(true);
6975
+ const from = ids.map((id) => state.status === "ready" ? state.snapshot.rows.find((r) => r.id === id)?.class : void 0).find((c) => c !== void 0) ?? "on-demand";
6976
+ const to = NEXT_CLASS[from];
6977
+ const moved = new Set(ids);
6978
+ setState((prev) => prev.status === "ready" ? {
6979
+ status: "ready",
6980
+ snapshot: { rows: prev.snapshot.rows.map((row) => moved.has(row.id) ? {
6981
+ ...row,
6982
+ class: to,
6983
+ classLabel: void 0
6984
+ } : row) }
6985
+ } : prev);
5650
6986
  try {
5651
6987
  const config = unwrap(await remote.getConfig(), "capabilityPolicy.getConfig");
5652
6988
  const key = kind === "skill" ? "skills" : "tools";
@@ -5660,13 +6996,11 @@ function CapabilitySection(props) {
5660
6996
  const raw = set && typeof set === "object" && !Array.isArray(set) ? set[cls] : void 0;
5661
6997
  lists[cls] = Array.isArray(raw) ? raw.filter((x) => typeof x === "string") : [];
5662
6998
  }
5663
- const from = ids.map((id) => state.status === "ready" ? state.snapshot.rows.find((r) => r.id === id)?.class : void 0).find((c) => c !== void 0) ?? "on-demand";
5664
- const to = NEXT_CLASS[from];
5665
6999
  for (const cls of CLASS_KEYS) lists[cls] = lists[cls].filter((id) => !ids.includes(id));
5666
7000
  lists[to] = [...lists[to], ...ids];
5667
7001
  const nextLists = {};
5668
7002
  for (const cls of CLASS_KEYS) nextLists[cls] = lists[cls];
5669
- await remote.updateConfig({ [key]: nextLists });
7003
+ unwrap(await remote.updateConfig({ [key]: nextLists }), "capabilityPolicy.updateConfig");
5670
7004
  const next = await loadSnapshot(remote);
5671
7005
  setState({
5672
7006
  status: "ready",
@@ -5697,7 +7031,22 @@ function CapabilitySection(props) {
5697
7031
  }),
5698
7032
  state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5699
7033
  className: "mc-error",
5700
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: state.message }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: `mountError=${String(mountError)}\nremoteKeys=${String(remoteKeys)}` })]
7034
+ children: [
7035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: state.message }),
7036
+ state.message.includes("Failed to fetch") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7037
+ className: "mc-error-hint",
7038
+ children: t("carrierFailureHint")
7039
+ }),
7040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7041
+ className: "mc-error-actions",
7042
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7043
+ type: "button",
7044
+ className: "mc-catalog-btn",
7045
+ onClick: () => void reload(),
7046
+ children: t("retry")
7047
+ })
7048
+ })
7049
+ ]
5701
7050
  }),
5702
7051
  notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5703
7052
  className: "mc-notice",
@@ -5712,18 +7061,33 @@ function CapabilitySection(props) {
5712
7061
  t,
5713
7062
  onTabChange: setActiveTab,
5714
7063
  onToggleServer: toggleServer,
5715
- onCycle: cycleClass
7064
+ onCycle: cycleClass,
7065
+ onRefresh: () => void refreshCatalog(),
7066
+ onNotice: setNotice
5716
7067
  })
5717
7068
  ]
5718
7069
  });
5719
7070
  }
5720
7071
  function ReadyBody(props) {
5721
- const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle } = props;
5722
- const { servers, skills } = groupRows(snapshot.rows);
5723
- const projectSkills = skills.filter((skill) => PROJECT_SOURCES.has(skill.source ?? ""));
5724
- const globalSkills = skills.filter((skill) => !PROJECT_SOURCES.has(skill.source ?? ""));
7072
+ const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh, onNotice } = props;
7073
+ /** Name filter, shared by both tabs: it narrows whichever list is shown. */
7074
+ const [filter, setFilter] = (0, react.useState)("");
7075
+ const nameFilter = compileNameFilter(filter);
7076
+ const needle = nameFilter.needle;
7077
+ const shownRows = filterRows(snapshot.rows, nameFilter, {
7078
+ builtIn: t("builtInGroup"),
7079
+ source: (source) => sourceLabel(source, t)
7080
+ });
7081
+ const { servers, skills } = groupRows(shownRows);
7082
+ const { presetSkills, presetGroups, projectSkills, globalSkills } = splitSkillGroups(skills);
5725
7083
  const [skillTab, setSkillTab] = (0, react.useState)("global");
5726
- const statRows = activeTab === "tools" ? snapshot.rows.filter((r) => r.kind === "tool") : skillTab === "project" ? projectSkills : globalSkills;
7084
+ const activeSkillTab = resolveSkillTab(skillTab, {
7085
+ presetSkills,
7086
+ presetGroups,
7087
+ projectSkills,
7088
+ globalSkills
7089
+ });
7090
+ const statRows = activeTab === "tools" ? shownRows.filter((r) => r.kind === "tool") : activeSkillTab === "project" ? projectSkills : activeSkillTab === "preset" ? presetSkills : globalSkills;
5727
7091
  const summary = CLASS_KEYS.map((cls) => ({
5728
7092
  cls,
5729
7093
  count: countByClass(statRows, cls)
@@ -5758,6 +7122,93 @@ function ReadyBody(props) {
5758
7122
  /** 能力目录弹层状态:查看三档策略配置 + 按需能力目录文件。 */
5759
7123
  const [catalogDocs, setCatalogDocs] = (0, react.useState)(null);
5760
7124
  const [catalogTab, setCatalogTab] = (0, react.useState)("policy");
7125
+ /** 注册能力 modal; its default sub-tab follows `activeTab`. */
7126
+ const [registerOpen, setRegisterOpen] = (0, react.useState)(false);
7127
+ /** 编辑 target; at most one is set. */
7128
+ const [editMcp, setEditMcp] = (0, react.useState)(void 0);
7129
+ const [editSkill, setEditSkill] = (0, react.useState)(void 0);
7130
+ /**
7131
+ * Skill directories registered under the skill root. Only those can be
7132
+ * edited: a project or bundled skill has no row to repoint, so its row gets
7133
+ * no 编辑 button rather than a button that always fails.
7134
+ */
7135
+ const [editableSkills, setEditableSkills] = (0, react.useState)(/* @__PURE__ */ new Set());
7136
+ const skillSignature = skills.map((skill) => `${skill.id}\u0000${skill.source ?? ""}`).join("");
7137
+ (0, react.useEffect)(() => {
7138
+ let cancelled = false;
7139
+ (async () => {
7140
+ try {
7141
+ const rows = unwrap(await remote.listSkillLocations(), "capabilityPolicy.listSkillLocations");
7142
+ if (!cancelled) setEditableSkills(new Set(rows.map((row) => row.skillName ?? row.name)));
7143
+ } catch {}
7144
+ })();
7145
+ return () => {
7146
+ cancelled = true;
7147
+ };
7148
+ }, [remote, skillSignature]);
7149
+ /** Open the 编辑 form for the MCP server behind a Tools-tab group header. */
7150
+ const openMcpEdit = (0, react.useCallback)(async (serverName) => {
7151
+ try {
7152
+ const found = unwrap(await remote.listLocations(), "capabilityPolicy.listLocations").find((row) => row.serverName === serverName);
7153
+ if (found === void 0) {
7154
+ onNotice(t("notEditable"));
7155
+ return;
7156
+ }
7157
+ setEditMcp(found);
7158
+ } catch (e) {
7159
+ onNotice(String(e));
7160
+ }
7161
+ }, [
7162
+ remote,
7163
+ t,
7164
+ onNotice
7165
+ ]);
7166
+ /**
7167
+ * Adopt a skill that dsh discovered from a user-level root this plugin reads
7168
+ * but does not manage: link its own directory into the user root so it becomes
7169
+ * an editable entry. The content is untouched.
7170
+ */
7171
+ const adoptSkill = (0, react.useCallback)(async (name) => {
7172
+ try {
7173
+ const entry = unwrap(await remote.adoptSkillLocation(name), "capabilityPolicy.adoptSkillLocation");
7174
+ onNotice(t("skillAdopted", { path: entry }));
7175
+ onRefresh();
7176
+ } catch (e) {
7177
+ onNotice(String(e));
7178
+ }
7179
+ }, [
7180
+ remote,
7181
+ t,
7182
+ onNotice,
7183
+ onRefresh
7184
+ ]);
7185
+ /**
7186
+ * Open the 编辑 form for a managed skill entry. `name` is the skill's declared
7187
+ * name, which is what the row and the panel address it by — an entry is
7188
+ * matched on that rather than on its own, possibly different, name. A bare
7189
+ * name can exist in both the user root and a project root, so the row's source
7190
+ * decides which entry the click meant; without it we fall back to the first
7191
+ * match.
7192
+ */
7193
+ const openSkillEdit = (0, react.useCallback)(async (name, source) => {
7194
+ try {
7195
+ const rows = unwrap(await remote.listSkillLocations(), "capabilityPolicy.listSkillLocations");
7196
+ const wanted = PROJECT_SKILL_SOURCES.has(source ?? "") ? "project" : "user";
7197
+ const named = rows.filter((row) => (row.skillName ?? row.name) === name);
7198
+ const found = named.find((row) => row.root === wanted) ?? named[0];
7199
+ if (found === void 0) {
7200
+ onNotice(t("notEditable"));
7201
+ return;
7202
+ }
7203
+ setEditSkill(found);
7204
+ } catch (e) {
7205
+ onNotice(String(e));
7206
+ }
7207
+ }, [
7208
+ remote,
7209
+ t,
7210
+ onNotice
7211
+ ]);
5761
7212
  /** Fetch and show the two read-only catalog documents. */
5762
7213
  const openCatalogDocs = (0, react.useCallback)(async () => {
5763
7214
  setCatalogDocs({ status: "loading" });
@@ -5779,9 +7230,17 @@ function ReadyBody(props) {
5779
7230
  className: "mc-heading",
5780
7231
  children: t("title")
5781
7232
  }),
5782
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5783
- className: "mc-desc",
5784
- children: t("desc")
7233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7234
+ className: "mc-desc-row",
7235
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7236
+ className: "mc-desc",
7237
+ children: t("desc")
7238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7239
+ type: "button",
7240
+ className: "mc-catalog-btn",
7241
+ onClick: () => void openCatalogDocs(),
7242
+ children: t("viewCatalog")
7243
+ })]
5785
7244
  }),
5786
7245
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5787
7246
  className: "mc-tabs",
@@ -5822,11 +7281,22 @@ function ReadyBody(props) {
5822
7281
  }, cls)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5823
7282
  type: "button",
5824
7283
  className: "mc-catalog-btn",
5825
- onClick: () => void openCatalogDocs(),
5826
- children: t("viewCatalog")
7284
+ onClick: () => setRegisterOpen(true),
7285
+ children: t("registerCapability")
5827
7286
  })]
5828
7287
  })]
5829
7288
  }),
7289
+ snapshot.rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7290
+ className: "mc-filter",
7291
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7292
+ type: "search",
7293
+ value: filter,
7294
+ placeholder: t("filterByName"),
7295
+ "aria-label": t("filterByName"),
7296
+ title: t("filterHint"),
7297
+ onChange: (e) => setFilter(e.target.value)
7298
+ })
7299
+ }),
5830
7300
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5831
7301
  role: "tabpanel",
5832
7302
  hidden: activeTab !== "tools",
@@ -5835,7 +7305,7 @@ function ReadyBody(props) {
5835
7305
  className: "mc-panel-inner",
5836
7306
  children: servers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5837
7307
  className: "mc-empty",
5838
- children: t("emptyTools")
7308
+ children: needle === "" ? t("emptyTools") : t("filterNoMatch")
5839
7309
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: servers.map(({ server, tools }) => {
5840
7310
  const open = openServers.has(server);
5841
7311
  const counts = CLASS_KEYS.map((cls) => ({
@@ -5863,15 +7333,15 @@ function ReadyBody(props) {
5863
7333
  }),
5864
7334
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5865
7335
  className: "mc-server-name",
5866
- children: server === BUILT_IN_SERVER ? t("builtInGroup") : server
7336
+ children: server === "built-in" ? t("builtInGroup") : server
5867
7337
  }),
5868
7338
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5869
7339
  className: "mc-server-count",
5870
7340
  children: t("toolCount", { count: tools.length })
5871
7341
  }),
5872
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5873
7343
  className: "mc-server-meta",
5874
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7344
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5875
7345
  className: "mc-counts",
5876
7346
  children: counts.filter(({ count }) => count > 0).map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5877
7347
  type: "button",
@@ -5892,7 +7362,16 @@ function ReadyBody(props) {
5892
7362
  count
5893
7363
  ]
5894
7364
  }, cls))
5895
- })
7365
+ }), server !== "built-in" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7366
+ type: "button",
7367
+ className: "mc-count mc-count--action",
7368
+ disabled: busy,
7369
+ onClick: (e) => {
7370
+ e.stopPropagation();
7371
+ openMcpEdit(server);
7372
+ },
7373
+ children: t("edit")
7374
+ })]
5896
7375
  })
5897
7376
  ]
5898
7377
  }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5943,55 +7422,88 @@ function ReadyBody(props) {
5943
7422
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5944
7423
  role: "tabpanel",
5945
7424
  hidden: activeTab !== "skills",
5946
- className: "mc-panel",
7425
+ className: "mc-panel mc-panel--tight",
5947
7426
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5948
7427
  className: "mc-panel-inner",
5949
7428
  children: skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5950
7429
  className: "mc-empty",
5951
- children: t("emptySkills")
7430
+ children: needle === "" ? t("emptySkills") : t("filterNoMatch")
5952
7431
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5953
7432
  className: "mc-subtabs",
5954
7433
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5955
7434
  className: "mc-tab-group",
5956
7435
  role: "tablist",
5957
7436
  "aria-label": t("skillsGroup"),
5958
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5959
- type: "button",
5960
- role: "tab",
5961
- className: "mc-tab",
5962
- "aria-selected": skillTab === "global",
5963
- "data-active": skillTab === "global" ? "true" : void 0,
5964
- onClick: () => setSkillTab("global"),
5965
- children: t("globalSkills")
5966
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5967
- type: "button",
5968
- role: "tab",
5969
- className: "mc-tab",
5970
- "aria-selected": skillTab === "project",
5971
- "data-active": skillTab === "project" ? "true" : void 0,
5972
- onClick: () => setSkillTab("project"),
5973
- children: t("projectSkills")
5974
- })]
7437
+ children: [
7438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7439
+ type: "button",
7440
+ role: "tab",
7441
+ className: "mc-tab",
7442
+ "aria-selected": activeSkillTab === "global",
7443
+ "data-active": activeSkillTab === "global" ? "true" : void 0,
7444
+ onClick: () => setSkillTab("global"),
7445
+ children: t("globalSkills")
7446
+ }),
7447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7448
+ type: "button",
7449
+ role: "tab",
7450
+ className: "mc-tab",
7451
+ "aria-selected": activeSkillTab === "project",
7452
+ "data-active": activeSkillTab === "project" ? "true" : void 0,
7453
+ onClick: () => setSkillTab("project"),
7454
+ children: t("projectSkills")
7455
+ }),
7456
+ presetSkills.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7457
+ type: "button",
7458
+ role: "tab",
7459
+ className: "mc-tab",
7460
+ "aria-selected": activeSkillTab === "preset",
7461
+ "data-active": activeSkillTab === "preset" ? "true" : void 0,
7462
+ onClick: () => setSkillTab("preset"),
7463
+ children: t("presetSkills")
7464
+ })
7465
+ ]
5975
7466
  })
5976
- }), skillTab === "global" ? globalSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7467
+ }), activeSkillTab === "global" ? globalSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
5977
7468
  skills: globalSkills,
5978
7469
  remote,
5979
7470
  busy,
5980
7471
  t,
5981
- onCycle
7472
+ onCycle,
7473
+ editableSkills,
7474
+ onEditSkill: openSkillEdit,
7475
+ onAdoptSkill: adoptSkill
5982
7476
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5983
7477
  className: "mc-empty",
5984
7478
  children: t("emptyGlobalSkills")
5985
- }) : projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7479
+ }) : activeSkillTab === "project" ? projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
5986
7480
  skills: projectSkills,
5987
7481
  remote,
5988
7482
  busy,
5989
7483
  t,
5990
- onCycle
7484
+ onCycle,
7485
+ editableSkills,
7486
+ onEditSkill: openSkillEdit,
7487
+ onAdoptSkill: adoptSkill
5991
7488
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5992
7489
  className: "mc-empty",
5993
7490
  children: t("emptyProjectSkills")
5994
- })] })
7491
+ }) : presetGroups.map(([preset, group]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7492
+ className: "mc-skill-group",
7493
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7494
+ className: "mc-skill-group-title",
7495
+ children: preset
7496
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
7497
+ skills: group,
7498
+ remote,
7499
+ busy,
7500
+ t,
7501
+ onCycle,
7502
+ editableSkills,
7503
+ onEditSkill: openSkillEdit,
7504
+ onAdoptSkill: adoptSkill
7505
+ })]
7506
+ }, preset))] })
5995
7507
  })
5996
7508
  }),
5997
7509
  toolDetail !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -6035,6 +7547,22 @@ function ReadyBody(props) {
6035
7547
  ]
6036
7548
  })
6037
7549
  }),
7550
+ (registerOpen || editMcp !== void 0 || editSkill !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocationModal, {
7551
+ remote,
7552
+ t,
7553
+ defaultKind: activeTab === "skills" ? "skill" : "mcp",
7554
+ ...editMcp !== void 0 ? { editMcp } : {},
7555
+ ...editSkill !== void 0 ? { editSkill } : {},
7556
+ onClose: () => {
7557
+ setRegisterOpen(false);
7558
+ setEditMcp(void 0);
7559
+ setEditSkill(void 0);
7560
+ },
7561
+ onChanged: (notice) => {
7562
+ onRefresh();
7563
+ if (notice !== void 0) onNotice(notice);
7564
+ }
7565
+ }),
6038
7566
  catalogDocs !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6039
7567
  className: "mc-preview-mask",
6040
7568
  onClick: () => setCatalogDocs(null),
@@ -6108,7 +7636,9 @@ function ReadyBody(props) {
6108
7636
  ] });
6109
7637
  }
6110
7638
  function SkillList(props) {
6111
- const { skills, remote, busy, t, onCycle } = props;
7639
+ const { skills, remote, busy, t, onCycle, editableSkills, onEditSkill, onAdoptSkill } = props;
7640
+ /** Skill id whose 纳入管理 is waiting for a confirmation click. */
7641
+ const [confirmAdopt, setConfirmAdopt] = (0, react.useState)(null);
6112
7642
  const [openSkill, setOpenSkill] = (0, react.useState)(null);
6113
7643
  const [tree, setTree] = (0, react.useState)({
6114
7644
  open: {},
@@ -6277,89 +7807,169 @@ function SkillList(props) {
6277
7807
  }, relPath);
6278
7808
  });
6279
7809
  };
6280
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => {
6281
- const open = openSkill === skill.id;
6282
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6283
- className: "mc-skill",
6284
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6285
- className: "mc-skill-row",
6286
- role: "button",
6287
- tabIndex: 0,
6288
- "aria-expanded": open,
6289
- onClick: () => void toggleSkill(skill.id),
6290
- onKeyDown: (e) => {
6291
- if (e.key === "Enter" || e.key === " ") {
6292
- e.preventDefault();
6293
- toggleSkill(skill.id);
6294
- }
6295
- },
6296
- children: [
6297
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
6298
- size: 12,
6299
- className: `mc-chevron${open ? " mc-chevron--open" : ""}`
6300
- }),
6301
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6302
- className: "mc-skill-name",
6303
- children: skill.name
6304
- }),
6305
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6306
- className: "mc-skill-meta",
6307
- children: [skill.mandatory && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6308
- className: "mc-tag",
6309
- children: t("mandatory")
6310
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6311
- type: "button",
6312
- className: `mc-count mc-count--${skill.class}`,
6313
- disabled: busy || skill.mandatory,
6314
- title: skill.classLabel,
6315
- onClick: (e) => {
6316
- e.stopPropagation();
6317
- onCycle([skill.id], "skill");
6318
- },
6319
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6320
- className: `mc-dot mc-dot--${skill.class}`,
6321
- "aria-hidden": "true"
6322
- }), t(CLASS_SHORT_KEYS[skill.class])]
6323
- })]
7810
+ /** The row the 纳入管理 confirmation is for, so the dialog can name it and
7811
+ show the directory the click will link. */
7812
+ const confirmAdoptRow = confirmAdopt === null ? void 0 : skills.find((row) => row.id === confirmAdopt);
7813
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7814
+ skills.map((skill) => {
7815
+ const open = openSkill === skill.id;
7816
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7817
+ className: "mc-skill",
7818
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7819
+ className: "mc-skill-row",
7820
+ role: "button",
7821
+ tabIndex: 0,
7822
+ "aria-expanded": open,
7823
+ onClick: () => void toggleSkill(skill.id),
7824
+ onKeyDown: (e) => {
7825
+ if (e.key === "Enter" || e.key === " ") {
7826
+ e.preventDefault();
7827
+ toggleSkill(skill.id);
7828
+ }
7829
+ },
7830
+ children: [
7831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
7832
+ size: 12,
7833
+ className: `mc-chevron${open ? " mc-chevron--open" : ""}`
7834
+ }),
7835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7836
+ className: "mc-skill-name",
7837
+ children: skill.name
7838
+ }),
7839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7840
+ className: "mc-skill-meta",
7841
+ children: [
7842
+ skill.mandatory && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7843
+ className: "mc-tag",
7844
+ children: t("mandatory")
7845
+ }),
7846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7847
+ type: "button",
7848
+ className: `mc-count mc-count--${skill.class}`,
7849
+ disabled: busy || skill.mandatory,
7850
+ title: skill.classLabel,
7851
+ onClick: (e) => {
7852
+ e.stopPropagation();
7853
+ onCycle([skill.id], "skill");
7854
+ },
7855
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7856
+ className: `mc-dot mc-dot--${skill.class}`,
7857
+ "aria-hidden": "true"
7858
+ }), t(CLASS_SHORT_KEYS[skill.class])]
7859
+ }),
7860
+ !editableSkills.has(skill.id) && (skill.preset !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7861
+ className: "mc-source",
7862
+ title: t("skillFromPresetHint"),
7863
+ children: t("skillFromPreset", { preset: skill.preset })
7864
+ }) : ADOPTABLE_SKILL_SOURCES.has(skill.source ?? "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7865
+ type: "button",
7866
+ className: "mc-count mc-count--action",
7867
+ disabled: busy,
7868
+ title: t("adoptSkillHint"),
7869
+ onClick: (e) => {
7870
+ e.stopPropagation();
7871
+ setConfirmAdopt(skill.id);
7872
+ },
7873
+ children: t("adoptSkill")
7874
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7875
+ className: "mc-source",
7876
+ title: t("skillSourceHint"),
7877
+ children: t("skillSource", { source: sourceLabel(skill.source, t) })
7878
+ })),
7879
+ editableSkills.has(skill.id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7880
+ type: "button",
7881
+ className: "mc-count mc-count--action",
7882
+ disabled: busy,
7883
+ onClick: (e) => {
7884
+ e.stopPropagation();
7885
+ onEditSkill(skill.id, skill.source);
7886
+ },
7887
+ children: t("edit")
7888
+ })
7889
+ ]
7890
+ })
7891
+ ]
7892
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7893
+ className: "mc-skill-body",
7894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7895
+ className: "mc-tree",
7896
+ children: renderEntries(tree.dirs[`${skill.id}:`], skill.id, "") ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7897
+ className: "mc-tree-row",
7898
+ children: "…"
7899
+ })
6324
7900
  })
6325
- ]
6326
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6327
- className: "mc-skill-body",
6328
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6329
- className: "mc-tree",
6330
- children: renderEntries(tree.dirs[`${skill.id}:`], skill.id, "") ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6331
- className: "mc-tree-row",
6332
- children: "…"
7901
+ })]
7902
+ }, skill.id);
7903
+ }),
7904
+ confirmAdopt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7905
+ className: "mc-preview-mask",
7906
+ onClick: () => setConfirmAdopt(null),
7907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7908
+ className: "mc-preview mc-confirm-dialog",
7909
+ onClick: (e) => e.stopPropagation(),
7910
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7911
+ className: "mc-preview-head",
7912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7913
+ className: "mc-preview-title",
7914
+ children: t("adoptSkillTitle", { name: confirmAdoptRow?.name ?? confirmAdopt })
6333
7915
  })
6334
- })
6335
- })]
6336
- }, skill.id);
6337
- }), preview !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6338
- className: "mc-preview-mask",
6339
- onClick: () => setPreview(null),
6340
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6341
- className: "mc-preview",
6342
- onClick: (e) => e.stopPropagation(),
6343
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6344
- className: "mc-preview-head",
6345
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6346
- className: "mc-preview-title",
6347
- children: preview.relPath
6348
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6349
- type: "button",
6350
- className: "mc-preview-close",
6351
- onClick: () => setPreview(null),
6352
- children: t("previewClose")
7916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7917
+ className: "mc-confirm-body",
7918
+ children: [
7919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("adoptSkillHint") }),
7920
+ confirmAdoptRow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7921
+ className: "mc-confirm-path",
7922
+ children: t("skillSource", { source: confirmAdoptRow.path ?? sourceLabel(confirmAdoptRow.source, t) })
7923
+ }),
7924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7925
+ className: "mc-confirm-actions",
7926
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7927
+ type: "button",
7928
+ className: "mc-catalog-btn",
7929
+ onClick: () => setConfirmAdopt(null),
7930
+ children: t("cancel")
7931
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7932
+ type: "button",
7933
+ className: "mc-catalog-btn",
7934
+ onClick: () => {
7935
+ const id = confirmAdopt;
7936
+ setConfirmAdopt(null);
7937
+ onAdoptSkill(id);
7938
+ },
7939
+ children: t("adoptSkill")
7940
+ })]
7941
+ })
7942
+ ]
6353
7943
  })]
6354
- }), preview.content !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
6355
- className: "mc-preview-body",
6356
- children: preview.content
6357
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6358
- className: "mc-preview-hint",
6359
- children: preview.error ?? "…"
6360
- })]
7944
+ })
7945
+ }),
7946
+ preview !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7947
+ className: "mc-preview-mask",
7948
+ onClick: () => setPreview(null),
7949
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7950
+ className: "mc-preview",
7951
+ onClick: (e) => e.stopPropagation(),
7952
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7953
+ className: "mc-preview-head",
7954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7955
+ className: "mc-preview-title",
7956
+ children: preview.relPath
7957
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7958
+ type: "button",
7959
+ className: "mc-preview-close",
7960
+ onClick: () => setPreview(null),
7961
+ children: t("previewClose")
7962
+ })]
7963
+ }), preview.content !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
7964
+ className: "mc-preview-body",
7965
+ children: preview.content
7966
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7967
+ className: "mc-preview-hint",
7968
+ children: preview.error ?? "…"
7969
+ })]
7970
+ })
6361
7971
  })
6362
- })] });
7972
+ ] });
6363
7973
  }
6364
7974
  //#endregion
6365
7975
  //#region src/client/index.ts
@@ -6374,28 +7984,29 @@ const inject = [
6374
7984
  "locale",
6375
7985
  "remote"
6376
7986
  ];
6377
- /** Register the 能力管理 section once `settings.section` is on the ledger. */
7987
+ /** Register the 能力菜单 section once `settings.section` is on the ledger. */
6378
7988
  async function apply(ctx) {
6379
7989
  const zh = {
6380
- nav: "能力管理",
6381
- title: "能力管理",
7990
+ nav: "能力菜单",
7991
+ title: "能力菜单",
6382
7992
  desc: "管理工具与技能的 常驻 / 按需 / 禁用 三档分类。",
6383
7993
  resident: "Resident(常驻上下文)",
6384
7994
  "on-demand": "On-demand(按需发现)",
6385
7995
  disabled: "Disabled(禁用)",
6386
- kind: "类型",
6387
- class: "分类",
6388
7996
  tool: "tool",
6389
7997
  skill: "skill",
6390
7998
  mandatory: "meta",
6391
- rules: "规则",
6392
7999
  toolsGroup: "Tools",
6393
8000
  skillsGroup: "Skills",
6394
8001
  builtInGroup: "系统内置",
6395
8002
  globalSkills: "全局技能",
6396
8003
  projectSkills: "项目技能",
8004
+ presetSkills: "预设技能",
6397
8005
  emptyGlobalSkills: "暂无全局技能",
6398
8006
  emptyProjectSkills: "暂无项目技能",
8007
+ filterByName: "按名字、分组或正则过滤…",
8008
+ filterHint: "忽略大小写。匹配工具/技能名、MCP server、技能来源标签与预设 id;也可以直接写正则,例如 cordis|ptc 或 /^mcp__dbx/。",
8009
+ filterNoMatch: "没有匹配的能力(清空过滤框可恢复完整列表)",
6399
8010
  emptyTools: "暂无工具",
6400
8011
  emptySkills: "暂无 Skill",
6401
8012
  toolCount: "{count} 个工具",
@@ -6407,10 +8018,69 @@ async function apply(ctx) {
6407
8018
  previewClose: "关闭",
6408
8019
  detailNotFound: "未找到该工具的详情",
6409
8020
  cycleOverridden: "分类未生效:{count} 个能力被更高优先级规则覆盖(如通配规则),可移除对应通配规则后重试",
6410
- viewCatalog: "查看能力目录",
8021
+ refreshFailed: "刷新失败,请查看日志",
8022
+ retry: "重试",
8023
+ carrierFailureHint: "请求未到达服务端:浏览器与 dsh 之间的连接已断开(Failed to fetch 属传输层失败,服务端方法未执行)。若刚重启过 dsh web,请先硬刷新页面(Ctrl/Cmd+Shift+R)再重试。",
8024
+ registerCapability: "注册能力",
8025
+ editMcp: "编辑 MCP 服务器",
8026
+ editSkillNamed: "编辑 Skill 目录:{name}",
8027
+ edit: "编辑",
8028
+ save: "保存",
8029
+ remove: "移除",
8030
+ cancel: "取消",
8031
+ confirmRemove: "确认移除",
8032
+ confirmRemoveMcp: "移除 MCP 服务器「{name}」?其 patch 文件中的配置行(含 headers、环境变量)将被删除,mcp__{name}__* 工具随之消失。",
8033
+ confirmRemoveSkillLink: "移除 Skill「{name}」?仅删除 ~/.dsh/skills 下的软链,源目录不受影响。",
8034
+ confirmRemoveSkillDir: "移除 Skill「{name}」?该项位于 ~/.dsh/skills 下且非软链,将连同文件递归删除,且不可恢复。",
8035
+ confirmSaveAnyway: "仍然保存",
8036
+ saveConfirmRealDir: "「{name}」这个条目是真实目录、不是软链:保存会先把它连同文件递归删除,再改为指向新路径,且不可恢复。",
8037
+ register: "注册",
8038
+ notEditable: "该项未在注册表中(可能是项目技能或内置技能),无法编辑。",
8039
+ entryNotFound: "目标已不存在,可能已被移除。",
8040
+ mcpServers: "MCP 服务器",
8041
+ skillDirs: "Skill 目录",
8042
+ skillDirPath: "技能来源路径",
8043
+ skillRoot: "技能位置",
8044
+ skillRootUser: "全局技能(~/.dsh/skills)",
8045
+ skillRootProject: "项目技能(<项目根>/.dsh/skills)",
8046
+ skillRootHintUser: "所有会话都能看到该技能。",
8047
+ skillRootHintProject: "只对 cwd 落在该项目内的会话可见。",
8048
+ skillProjectPath: "目标项目路径",
8049
+ skillProjectPathHint: "项目内任意一个已存在的路径即可;项目根按 dsh 的规则确定(向上找最近的 .git),技能写进 <项目根>/.dsh/skills。",
8050
+ skillRegisteredAt: "已注册到 {path}",
8051
+ skillRepointed: "已改为指向 {dir}",
8052
+ skillAdopted: "已纳入管理:{path}",
8053
+ skillSource: "来源:{source}",
8054
+ skillSourceHint: "该技能由 dsh 从其它根发现,不在本插件管理的技能目录里,因此没有「编辑」。",
8055
+ skillFromPreset: "来自预设 {preset}",
8056
+ skillFromPresetHint: "该技能随 agent preset 分发,只对挂载了该预设的会话可见;不能纳入管理(那会把它软链进全局技能根,对所有会话生效)。",
8057
+ skillUnmanaged: "未纳入管理",
8058
+ sourceCustom: "自定义技能目录",
8059
+ sourceBundled: "随 dsh 预置",
8060
+ adoptSkill: "纳入管理",
8061
+ adoptSkillHint: "把它软链进 dsh 的技能根目录(默认 ~/.dsh/skills),内容不动;之后就能编辑与移除了。",
8062
+ adoptSkillTitle: "纳入管理「{name}」?",
8063
+ skillDirHint: "目录内需有 SKILL.md(按 dsh 的 skill 格式校验)。技能名以 SKILL.md 里声明的为准,目录名只决定注册后的软链名,两者不同也可以。",
8064
+ serverName: "serverName",
8065
+ serverNameImmutable: "serverName 不可修改:它构成工具名前缀 mcp__<serverName>__<tool>,并已被既有会话历史与权限规则引用;修改后这些记录将不再匹配。",
8066
+ transport: "传输方式",
8067
+ transportStdio: "stdio(本地子进程)",
8068
+ transportHttp: "streamable-http(HTTP MCP 端点)",
8069
+ transportHintStdio: "由客户端将该 MCP server 作为子进程启动,JSON-RPC 报文经标准输入 / 输出交换。",
8070
+ transportHintHttp: "该 MCP server 作为独立进程运行,可服务多个客户端连接;客户端经单个支持 POST / GET 的 HTTP 端点(MCP endpoint)与其通信。",
8071
+ command: "命令",
8072
+ args: "参数(空格分隔)",
8073
+ cwd: "工作目录",
8074
+ env: "环境变量",
8075
+ url: "URL",
8076
+ headers: "请求头",
8077
+ headersHint: "每行一个 Header,格式 Key: Value;Bearer token 等认证凭据在此填写。",
8078
+ timeout: "超时(秒)",
8079
+ timeoutInvalid: "超时须为数字,单位为秒。",
8080
+ viewCatalog: "策略与目录",
6411
8081
  catalogPolicy: "三档策略配置",
6412
8082
  catalogOnDemand: "按需能力目录",
6413
- catalogPolicyNote: "实时生成,只读;持久化入口为 cordis.patch.yml(未列出规则的能力默认常驻)",
8083
+ catalogPolicyNote: "实时生成;点选改动会在停手后自动写回 cordis.patch.yml(未列出规则的能力默认常驻)",
6414
8084
  catalogDisabled: "按需能力目录未启用(catalogFile 为空)",
6415
8085
  catalogUnreadable: "按需能力目录文件读取失败"
6416
8086
  };
@@ -6421,19 +8091,20 @@ async function apply(ctx) {
6421
8091
  resident: "Resident",
6422
8092
  "on-demand": "On-demand",
6423
8093
  disabled: "Disabled",
6424
- kind: "kind",
6425
- class: "class",
6426
8094
  tool: "tool",
6427
8095
  skill: "skill",
6428
8096
  mandatory: "meta",
6429
- rules: "rules",
6430
8097
  toolsGroup: "Tools",
6431
8098
  skillsGroup: "Skills",
6432
8099
  builtInGroup: "System built-in",
6433
8100
  globalSkills: "Global skills",
6434
8101
  projectSkills: "Project skills",
8102
+ presetSkills: "Preset skills",
6435
8103
  emptyGlobalSkills: "No global skills",
6436
8104
  emptyProjectSkills: "No project skills",
8105
+ filterByName: "Filter by name, group, or regex…",
8106
+ 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/.",
8107
+ filterNoMatch: "No capability matches (clear the filter to see the full list)",
6437
8108
  emptyTools: "No tools",
6438
8109
  emptySkills: "No skills",
6439
8110
  toolCount: "{count} tools",
@@ -6445,10 +8116,69 @@ async function apply(ctx) {
6445
8116
  previewClose: "Close",
6446
8117
  detailNotFound: "Tool detail not found",
6447
8118
  cycleOverridden: "Classification not applied: {count} capability(ies) overridden by a higher-priority rule (e.g. a wildcard). Remove the matching wildcard rule and retry.",
6448
- viewCatalog: "View capability catalog",
8119
+ refreshFailed: "Refresh failed; see the log",
8120
+ retry: "Retry",
8121
+ 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.",
8122
+ registerCapability: "Register capability",
8123
+ editMcp: "Edit MCP server",
8124
+ editSkillNamed: "Edit skill directory: {name}",
8125
+ edit: "Edit",
8126
+ save: "Save",
8127
+ remove: "Remove",
8128
+ cancel: "Cancel",
8129
+ confirmRemove: "Confirm removal",
8130
+ confirmRemoveMcp: "Remove the MCP server \"{name}\"? Its config row in the patch file (headers and environment included) is deleted, and the mcp__{name}__* tools disappear with it.",
8131
+ confirmRemoveSkillLink: "Unregister the skill \"{name}\"? Only the symlink under ~/.dsh/skills is removed; the source directory is left untouched.",
8132
+ confirmRemoveSkillDir: "Remove the skill \"{name}\"? It sits under ~/.dsh/skills and is not a symlink, so it will be deleted recursively with all of its files. This cannot be undone.",
8133
+ confirmSaveAnyway: "Save anyway",
8134
+ saveConfirmRealDir: "The entry \"{name}\" is a real directory, not a symlink: saving deletes it and its files recursively before repointing it, and that cannot be undone.",
8135
+ register: "Register",
8136
+ notEditable: "This entry is not in the registry (a project or bundled skill), so it cannot be edited.",
8137
+ entryNotFound: "The target no longer exists; it may have been removed.",
8138
+ mcpServers: "MCP servers",
8139
+ skillDirs: "Skill directories",
8140
+ skillDirPath: "Skill source path",
8141
+ skillRoot: "Skill location",
8142
+ skillRootUser: "Global (~/.dsh/skills)",
8143
+ skillRootProject: "Project (<projectRoot>/.dsh/skills)",
8144
+ skillRootHintUser: "Visible to every session.",
8145
+ skillRootHintProject: "Visible only to sessions whose cwd sits inside that project.",
8146
+ skillProjectPath: "Target project path",
8147
+ skillProjectPathHint: "Any existing path inside the project; the project root is derived the way dsh derives it (nearest .git above it) and the skill is written under <projectRoot>/.dsh/skills.",
8148
+ skillRegisteredAt: "Registered at {path}",
8149
+ skillRepointed: "Repointed to {dir}",
8150
+ skillAdopted: "Adopted: {path}",
8151
+ skillSource: "Source: {source}",
8152
+ skillSourceHint: "dsh found this skill in another root, not in a skill directory this plugin manages, so there is no Edit button.",
8153
+ skillFromPreset: "From preset {preset}",
8154
+ 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).",
8155
+ skillUnmanaged: "Not managed here",
8156
+ sourceCustom: "custom skill dirs",
8157
+ sourceBundled: "bundled with dsh",
8158
+ adoptSkill: "Adopt",
8159
+ adoptSkillHint: "Link it into dsh's skill root (default ~/.dsh/skills); the content is untouched, and it becomes editable and removable.",
8160
+ adoptSkillTitle: "Adopt \"{name}\"?",
8161
+ 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.",
8162
+ serverName: "serverName",
8163
+ 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.",
8164
+ transport: "Transport",
8165
+ transportStdio: "stdio (local subprocess)",
8166
+ transportHttp: "streamable-http (HTTP MCP endpoint)",
8167
+ transportHintStdio: "The client launches the MCP server as a subprocess and exchanges JSON-RPC messages over its standard input and output.",
8168
+ 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.",
8169
+ command: "Command",
8170
+ args: "Args (space separated)",
8171
+ cwd: "Working directory",
8172
+ env: "Environment",
8173
+ url: "URL",
8174
+ headers: "Headers",
8175
+ headersHint: "One header per line as Key: Value; credentials such as a Bearer token go here.",
8176
+ timeout: "Timeout (seconds)",
8177
+ timeoutInvalid: "Timeout must be a number of seconds.",
8178
+ viewCatalog: "Policy & catalog",
6449
8179
  catalogPolicy: "Policy (effective)",
6450
8180
  catalogOnDemand: "On-demand catalog",
6451
- catalogPolicyNote: "Generated live from the effective policy (read-only); persist via cordis.patch.yml. Capabilities without a rule default to Resident.",
8181
+ catalogPolicyNote: "Generated live from the effective policy; clicked changes are written back to cordis.patch.yml once you stop. Capabilities without a rule default to Resident.",
6452
8182
  catalogDisabled: "On-demand catalog emission is disabled (catalogFile is empty).",
6453
8183
  catalogUnreadable: "Failed to read the on-demand catalog file."
6454
8184
  };
@@ -6473,18 +8203,45 @@ async function apply(ctx) {
6473
8203
  return;
6474
8204
  }
6475
8205
  };
8206
+ /**
8207
+ * The two signals that say "the host catalog may have moved, and this page did
8208
+ * not ask": a settings document edit, and a carrier reconnect.
8209
+ *
8210
+ * `settings/document-updated` is one of the events dsh forwards to browsers
8211
+ * (so it arrives over the same validated channel as the remote calls) and it
8212
+ * fires for external edits too — composition files are edited outside the
8213
+ * browser, and nothing else on the wire announces that. `connection/reset` is
8214
+ * the client's own reconnect signal rather than a forwarded one, which is why
8215
+ * it is a plain local `ctx.on`; dsh's own settings sections listen to exactly
8216
+ * this pair, including the first connection, which closes the window where a
8217
+ * change lands between the initial read and the subscription.
8218
+ *
8219
+ * Neither subscription is fatal: the section falls back to its version poll.
8220
+ */
8221
+ const subscribeSignals = (listener) => {
8222
+ const disposers = [];
8223
+ const forwarded = ctx.remote;
8224
+ try {
8225
+ if (typeof forwarded.$on !== "function") throw new Error("ctx.remote.$on is unavailable");
8226
+ disposers.push(forwarded.$on("settings/document-updated", () => listener()));
8227
+ } catch (error) {
8228
+ console.error("[capability-menu] settings/document-updated subscription failed:", error);
8229
+ }
8230
+ try {
8231
+ disposers.push(ctx.on("connection/reset", () => listener()));
8232
+ } catch (error) {
8233
+ console.error("[capability-menu] connection/reset subscription failed:", error);
8234
+ }
8235
+ return () => {
8236
+ for (const dispose of disposers) dispose();
8237
+ };
8238
+ };
6476
8239
  const injected = () => {
6477
- const namespace = remote();
6478
- const remoteKeys = namespace == null ? void 0 : Object.keys(namespace).filter((k) => [
6479
- "getConfig",
6480
- "updateConfig",
6481
- "classifyAll"
6482
- ].includes(k)).join(",");
6483
8240
  return {
6484
- remote: namespace,
8241
+ remote: remote(),
6485
8242
  t,
6486
- ...mountError !== void 0 ? { mountError } : {},
6487
- ...remoteKeys !== void 0 ? { remoteKeys } : {}
8243
+ subscribeSignals,
8244
+ ...mountError !== void 0 ? { mountError } : {}
6488
8245
  };
6489
8246
  };
6490
8247
  ctx.slots.inject("settings.section", () => ctx.slots.register({