@daweifu/capability-menu 0.1.2 → 0.1.3

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,6 +5190,50 @@ 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(),
@@ -5257,7 +5301,7 @@ const TYPERT_REMOTE = {
5257
5301
  },
5258
5302
  sourceLocation: {
5259
5303
  file: "src/server/remote.ts",
5260
- line: 60,
5304
+ line: 61,
5261
5305
  column: 3
5262
5306
  }
5263
5307
  },
@@ -5284,7 +5328,7 @@ const TYPERT_REMOTE = {
5284
5328
  },
5285
5329
  sourceLocation: {
5286
5330
  file: "src/server/remote.ts",
5287
- line: 66,
5331
+ line: 67,
5288
5332
  column: 3
5289
5333
  }
5290
5334
  },
@@ -5302,7 +5346,25 @@ const TYPERT_REMOTE = {
5302
5346
  },
5303
5347
  sourceLocation: {
5304
5348
  file: "src/server/remote.ts",
5305
- line: 72,
5349
+ line: 73,
5350
+ column: 3
5351
+ }
5352
+ },
5353
+ {
5354
+ id: "@daweifu/capability-menu#capabilityPolicy/refresh",
5355
+ service: "capabilityPolicy",
5356
+ namespace: "capabilityPolicy",
5357
+ method: "refresh",
5358
+ invocation: { kind: "direct" },
5359
+ parameters: [],
5360
+ result: {
5361
+ mode: "strict",
5362
+ typeSymbol: "void",
5363
+ schema: _undefined()
5364
+ },
5365
+ sourceLocation: {
5366
+ file: "src/server/remote.ts",
5367
+ line: 84,
5306
5368
  column: 3
5307
5369
  }
5308
5370
  },
@@ -5339,7 +5401,7 @@ const TYPERT_REMOTE = {
5339
5401
  },
5340
5402
  sourceLocation: {
5341
5403
  file: "src/server/remote.ts",
5342
- line: 84,
5404
+ line: 85,
5343
5405
  column: 3
5344
5406
  }
5345
5407
  },
@@ -5375,7 +5437,7 @@ const TYPERT_REMOTE = {
5375
5437
  },
5376
5438
  sourceLocation: {
5377
5439
  file: "src/server/remote.ts",
5378
- line: 90,
5440
+ line: 91,
5379
5441
  column: 3
5380
5442
  }
5381
5443
  },
@@ -5402,7 +5464,7 @@ const TYPERT_REMOTE = {
5402
5464
  },
5403
5465
  sourceLocation: {
5404
5466
  file: "src/server/remote.ts",
5405
- line: 78,
5467
+ line: 79,
5406
5468
  column: 3
5407
5469
  }
5408
5470
  },
@@ -5420,7 +5482,284 @@ const TYPERT_REMOTE = {
5420
5482
  },
5421
5483
  sourceLocation: {
5422
5484
  file: "src/server/remote.ts",
5423
- line: 100,
5485
+ line: 104,
5486
+ column: 3
5487
+ }
5488
+ },
5489
+ {
5490
+ id: "@daweifu/capability-menu#capabilityPolicy/listLocations",
5491
+ service: "capabilityPolicy",
5492
+ namespace: "capabilityPolicy",
5493
+ method: "listLocations",
5494
+ invocation: { kind: "direct" },
5495
+ parameters: [],
5496
+ result: {
5497
+ mode: "strict",
5498
+ typeSymbol: "@daweifu/capability-menu#McpLocation[]",
5499
+ schema: array(mcpLocation$schema)
5500
+ },
5501
+ sourceLocation: {
5502
+ file: "src/server/remote.ts",
5503
+ line: 193,
5504
+ column: 3
5505
+ }
5506
+ },
5507
+ {
5508
+ id: "@daweifu/capability-menu#capabilityPolicy/addLocation",
5509
+ service: "capabilityPolicy",
5510
+ namespace: "capabilityPolicy",
5511
+ method: "addLocation",
5512
+ invocation: { kind: "direct" },
5513
+ parameters: [{
5514
+ name: "input",
5515
+ wire: "input",
5516
+ source: "json",
5517
+ codec: {
5518
+ mode: "strict",
5519
+ typeSymbol: "@daweifu/capability-menu#McpInput",
5520
+ schema: mcpInput$schema
5521
+ }
5522
+ }],
5523
+ result: {
5524
+ mode: "strict",
5525
+ typeSymbol: "string",
5526
+ schema: string()
5527
+ },
5528
+ sourceLocation: {
5529
+ file: "src/server/remote.ts",
5530
+ line: 199,
5531
+ column: 3
5532
+ }
5533
+ },
5534
+ {
5535
+ id: "@daweifu/capability-menu#capabilityPolicy/removeLocation",
5536
+ service: "capabilityPolicy",
5537
+ namespace: "capabilityPolicy",
5538
+ method: "removeLocation",
5539
+ invocation: { kind: "direct" },
5540
+ parameters: [{
5541
+ name: "id",
5542
+ wire: "id",
5543
+ source: "json",
5544
+ codec: {
5545
+ mode: "strict",
5546
+ typeSymbol: "string",
5547
+ schema: string()
5548
+ }
5549
+ }],
5550
+ result: {
5551
+ mode: "strict",
5552
+ typeSymbol: "boolean",
5553
+ schema: boolean()
5554
+ },
5555
+ sourceLocation: {
5556
+ file: "src/server/remote.ts",
5557
+ line: 205,
5558
+ column: 3
5559
+ }
5560
+ },
5561
+ {
5562
+ id: "@daweifu/capability-menu#capabilityPolicy/updateLocation",
5563
+ service: "capabilityPolicy",
5564
+ namespace: "capabilityPolicy",
5565
+ method: "updateLocation",
5566
+ invocation: { kind: "direct" },
5567
+ parameters: [{
5568
+ name: "id",
5569
+ wire: "id",
5570
+ source: "json",
5571
+ codec: {
5572
+ mode: "strict",
5573
+ typeSymbol: "string",
5574
+ schema: string()
5575
+ }
5576
+ }, {
5577
+ name: "input",
5578
+ wire: "input",
5579
+ source: "json",
5580
+ codec: {
5581
+ mode: "strict",
5582
+ typeSymbol: "@daweifu/capability-menu#McpUpdateInput",
5583
+ schema: mcpUpdateInput$schema
5584
+ }
5585
+ }],
5586
+ result: {
5587
+ mode: "strict",
5588
+ typeSymbol: "boolean",
5589
+ schema: boolean()
5590
+ },
5591
+ sourceLocation: {
5592
+ file: "src/server/remote.ts",
5593
+ line: 211,
5594
+ column: 3
5595
+ }
5596
+ },
5597
+ {
5598
+ id: "@daweifu/capability-menu#capabilityPolicy/listSkillLocations",
5599
+ service: "capabilityPolicy",
5600
+ namespace: "capabilityPolicy",
5601
+ method: "listSkillLocations",
5602
+ invocation: { kind: "direct" },
5603
+ parameters: [],
5604
+ result: {
5605
+ mode: "strict",
5606
+ typeSymbol: "@daweifu/capability-menu#SkillLocation[]",
5607
+ schema: array(skillLocation$schema)
5608
+ },
5609
+ sourceLocation: {
5610
+ file: "src/server/remote.ts",
5611
+ line: 229,
5612
+ column: 3
5613
+ }
5614
+ },
5615
+ {
5616
+ id: "@daweifu/capability-menu#capabilityPolicy/addSkillLocation",
5617
+ service: "capabilityPolicy",
5618
+ namespace: "capabilityPolicy",
5619
+ method: "addSkillLocation",
5620
+ invocation: { kind: "direct" },
5621
+ parameters: [{
5622
+ name: "dir",
5623
+ wire: "dir",
5624
+ source: "json",
5625
+ codec: {
5626
+ mode: "strict",
5627
+ typeSymbol: "string",
5628
+ schema: string()
5629
+ }
5630
+ }, {
5631
+ name: "projectPath",
5632
+ wire: "projectPath",
5633
+ source: "json",
5634
+ acceptsUndefined: true,
5635
+ codec: {
5636
+ mode: "strict",
5637
+ typeSymbol: "string",
5638
+ schema: string().optional()
5639
+ }
5640
+ }],
5641
+ result: {
5642
+ mode: "strict",
5643
+ typeSymbol: "string",
5644
+ schema: string()
5645
+ },
5646
+ sourceLocation: {
5647
+ file: "src/server/remote.ts",
5648
+ line: 252,
5649
+ column: 3
5650
+ }
5651
+ },
5652
+ {
5653
+ id: "@daweifu/capability-menu#capabilityPolicy/removeSkillLocation",
5654
+ service: "capabilityPolicy",
5655
+ namespace: "capabilityPolicy",
5656
+ method: "removeSkillLocation",
5657
+ invocation: { kind: "direct" },
5658
+ parameters: [{
5659
+ name: "name",
5660
+ wire: "name",
5661
+ source: "json",
5662
+ codec: {
5663
+ mode: "strict",
5664
+ typeSymbol: "string",
5665
+ schema: string()
5666
+ }
5667
+ }, {
5668
+ name: "entryDir",
5669
+ wire: "entryDir",
5670
+ source: "json",
5671
+ acceptsUndefined: true,
5672
+ codec: {
5673
+ mode: "strict",
5674
+ typeSymbol: "string",
5675
+ schema: string().optional()
5676
+ }
5677
+ }],
5678
+ result: {
5679
+ mode: "strict",
5680
+ typeSymbol: "boolean",
5681
+ schema: boolean()
5682
+ },
5683
+ sourceLocation: {
5684
+ file: "src/server/remote.ts",
5685
+ line: 258,
5686
+ column: 3
5687
+ }
5688
+ },
5689
+ {
5690
+ id: "@daweifu/capability-menu#capabilityPolicy/adoptSkillLocation",
5691
+ service: "capabilityPolicy",
5692
+ namespace: "capabilityPolicy",
5693
+ method: "adoptSkillLocation",
5694
+ invocation: { kind: "direct" },
5695
+ parameters: [{
5696
+ name: "name",
5697
+ wire: "name",
5698
+ source: "json",
5699
+ codec: {
5700
+ mode: "strict",
5701
+ typeSymbol: "string",
5702
+ schema: string()
5703
+ }
5704
+ }],
5705
+ result: {
5706
+ mode: "strict",
5707
+ typeSymbol: "string",
5708
+ schema: string()
5709
+ },
5710
+ sourceLocation: {
5711
+ file: "src/server/remote.ts",
5712
+ line: 259,
5713
+ column: 3
5714
+ }
5715
+ },
5716
+ {
5717
+ id: "@daweifu/capability-menu#capabilityPolicy/updateSkillLocation",
5718
+ service: "capabilityPolicy",
5719
+ namespace: "capabilityPolicy",
5720
+ method: "updateSkillLocation",
5721
+ invocation: { kind: "direct" },
5722
+ parameters: [
5723
+ {
5724
+ name: "name",
5725
+ wire: "name",
5726
+ source: "json",
5727
+ codec: {
5728
+ mode: "strict",
5729
+ typeSymbol: "string",
5730
+ schema: string()
5731
+ }
5732
+ },
5733
+ {
5734
+ name: "dir",
5735
+ wire: "dir",
5736
+ source: "json",
5737
+ codec: {
5738
+ mode: "strict",
5739
+ typeSymbol: "string",
5740
+ schema: string()
5741
+ }
5742
+ },
5743
+ {
5744
+ name: "entryDir",
5745
+ wire: "entryDir",
5746
+ source: "json",
5747
+ acceptsUndefined: true,
5748
+ codec: {
5749
+ mode: "strict",
5750
+ typeSymbol: "string",
5751
+ schema: string().optional()
5752
+ }
5753
+ }
5754
+ ],
5755
+ result: {
5756
+ mode: "strict",
5757
+ typeSymbol: "boolean",
5758
+ schema: boolean()
5759
+ },
5760
+ sourceLocation: {
5761
+ file: "src/server/remote.ts",
5762
+ line: 264,
5424
5763
  column: 3
5425
5764
  }
5426
5765
  }
@@ -5433,16 +5772,600 @@ function unwrap(result, what) {
5433
5772
  if (!result.ok) throw new Error(`${what} failed: ${result.error.code}: ${result.error.message}`);
5434
5773
  return result.value;
5435
5774
  }
5436
- /** Load the classification list from the remote. */
5775
+ /**
5776
+ * Unwrap a RemoteResult-like for a user-facing mutation.
5777
+ *
5778
+ * The policy service's messages are already written for the person at the
5779
+ * dialog (Chinese, naming the offending path), so the RPC name and error code
5780
+ * that `unwrap` adds for diagnostics are dropped here: they would surface as
5781
+ * `capabilityPolicy.addSkillLocation failed: <code>: 目录中没有 SKILL.md:…`
5782
+ * in a dialog that is otherwise plain Chinese.
5783
+ */
5784
+ function unwrapMessage(result) {
5785
+ if (!result.ok) throw new Error(result.error.message);
5786
+ return result.value;
5787
+ }
5788
+ /**
5789
+ * The last successful snapshot, at module scope.
5790
+ *
5791
+ * The section is remounted every time the settings tab is opened, and a cold
5792
+ * `classifyAll` can be slow once per page load (the plugin's mount awaits a full
5793
+ * catalog enumeration before the surface has data). Keeping the previous rows
5794
+ * lets a remount paint them immediately and revalidate behind the user instead
5795
+ * of showing a loading placeholder. This is a view cache only — every read is
5796
+ * still followed by an authoritative one.
5797
+ */
5798
+ let lastSnapshot;
5799
+ /** The previously loaded snapshot, when this page session has one. */
5800
+ function cachedSnapshot() {
5801
+ return lastSnapshot;
5802
+ }
5803
+ /** Load the classification list from the remote, remembering it for remounts. */
5437
5804
  async function loadSnapshot(remote) {
5438
- return { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
5805
+ lastSnapshot = { rows: unwrap(await remote.classifyAll(), "capabilityPolicy.classifyAll") };
5806
+ return lastSnapshot;
5807
+ }
5808
+ //#endregion
5809
+ //#region src/client/LocationModal.tsx
5810
+ /**
5811
+ * 能力来源弹窗:注册与编辑 MCP 服务器 / Skill 目录。
5812
+ *
5813
+ * Two modes, one form implementation:
5814
+ *
5815
+ * - **注册** — opened from the header's 注册能力 button. Carries the two
5816
+ * sub-tabs (MCP servers / skill directories) and opens on the one matching
5817
+ * the main tab the user is on, so one button is context-sensitive. No list of
5818
+ * what is already registered: that lives on the entries themselves, behind
5819
+ * the per-row 编辑 buttons, where it is actually actionable.
5820
+ * - **编辑** — opened from a row's 编辑 button with the entry prefilled.
5821
+ *
5822
+ * Only 注册 / 保存 / 移除 are offered. Temporarily silencing a source is the
5823
+ * exposure policy's job (classify the capability as Disabled), so there is no
5824
+ * second enable switch here.
5825
+ */
5826
+ const CSS_ID$1 = "capability-menu-location-css";
5827
+ const CSS$1 = `
5828
+ .lm-block{display:flex;flex-direction:column;gap:8px}
5829
+ .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}
5830
+ .lm-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
5831
+ .lm-btn--danger:hover{color:#b3261e;border-color:#b3261e}
5832
+ .lm-btn:disabled{opacity:.55;cursor:default}
5833
+ /* Each field is its own two-column grid with a fixed label track.
5834
+ A single form-wide grid looked tidier but was fragile: it needed
5835
+ .lm-field{display:contents}, which turned every hint into a free-floating grid
5836
+ item, and one extra half-width item pushed the following labels into column 2 and
5837
+ their inputs into column 1 — which is exactly how the streamable-http layout got
5838
+ scrambled (that branch has an odd number of items). Per-field grids mean a hint
5839
+ can only ever affect its own field, and a fixed label track (rather than
5840
+ min-width, which sizes per row) keeps every label column identical. */
5841
+ .lm-form{display:flex;flex-direction:column;gap:6px}
5842
+ .lm-field{--lm-label-w:104px;display:grid;grid-template-columns:var(--lm-label-w) 1fr;gap:2px 8px;align-items:start}
5843
+ .lm-label{grid-column:1;min-width:0;overflow-wrap:anywhere;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
5844
+ /* width:100% must come with border-box: the field carries a 1px border plus 8px of
5845
+ padding, which would otherwise overflow the column and clip. */
5846
+ .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}
5847
+ .lm-input:disabled{opacity:.6}
5848
+ textarea.lm-input{resize:vertical;font-family:var(--dsw-font-markdown-code-block-font-family)}
5849
+ /* Lives inside its field, in the content column: it lines up with the box it
5850
+ annotates and takes the caption colour so it does not read as another label. */
5851
+ .lm-hint{grid-column:2;margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}
5852
+ .lm-error{margin:0;font-size:12px;color:#b3261e;word-break:break-word}
5853
+ .lm-actions{display:flex;gap:8px;justify-content:flex-end;padding:4px 0 0}
5854
+ /* Removal is irreversible, so it takes over the action row for one confirmation
5855
+ step: the message states the consequence, then the buttons act. */
5856
+ .lm-confirm{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border:1px solid #b3261e;border-radius:6px}
5857
+ .lm-confirm-msg{flex:1 1 auto;margin:0;font-size:12px;line-height:18px;word-break:break-word}
5858
+ .lm-confirm-actions{display:flex;gap:8px;flex-shrink:0}
5859
+ /* The modal shell itself carries no padding (it is shared with the read-only
5860
+ catalog viewer, whose body supplies its own); the form supplies it here. */
5861
+ .lm-body{padding:14px 16px;overflow:auto;flex:1 1 auto;min-height:0}
5862
+ `;
5863
+ if (typeof document !== "undefined" && document.querySelector(`style[data-css-id="${CSS_ID$1}"]`) === null) {
5864
+ const tag = document.createElement("style");
5865
+ tag.dataset.cssId = CSS_ID$1;
5866
+ tag.textContent = CSS$1;
5867
+ document.head.appendChild(tag);
5868
+ }
5869
+ /** Render a key/value map as `Key: Value` lines. */
5870
+ function toLines(record) {
5871
+ if (record === void 0) return "";
5872
+ return Object.entries(record).map(([key, value]) => `${key}: ${value}`).join("\n");
5873
+ }
5874
+ /** Parse `Key: Value` lines into a map; blank lines and lines without `:` are skipped. */
5875
+ function fromLines(text) {
5876
+ const out = {};
5877
+ for (const line of text.split("\n")) {
5878
+ const trimmed = line.trim();
5879
+ if (trimmed.length === 0) continue;
5880
+ const at = trimmed.indexOf(":");
5881
+ if (at <= 0) continue;
5882
+ const key = trimmed.slice(0, at).trim();
5883
+ const value = trimmed.slice(at + 1).trim();
5884
+ if (key.length > 0) out[key] = value;
5885
+ }
5886
+ return Object.keys(out).length > 0 ? out : void 0;
5887
+ }
5888
+ function LocationModal(props) {
5889
+ const { remote, t, defaultKind = "mcp", editMcp, editSkill, onClose, onChanged } = props;
5890
+ const editing = editMcp !== void 0 || editSkill !== void 0;
5891
+ const [subTab, setSubTab] = (0, react.useState)(editSkill !== void 0 ? "skill" : editMcp !== void 0 ? "mcp" : defaultKind);
5892
+ const [error, setError] = (0, react.useState)(null);
5893
+ const [busy, setBusy] = (0, react.useState)(false);
5894
+ /**
5895
+ * Which action is waiting for confirmation, if any. Two need one: 移除, and
5896
+ * 保存 on an entry that is a real directory — repointing unlinks the entry
5897
+ * first, which recursively deletes a real directory's contents.
5898
+ */
5899
+ const [confirm, setConfirm] = (0, react.useState)(null);
5900
+ const [serverName, setServerName] = (0, react.useState)(editMcp?.serverName ?? "");
5901
+ const [transport, setTransport] = (0, react.useState)(editMcp?.transport ?? "streamable-http");
5902
+ const [command, setCommand] = (0, react.useState)(editMcp?.command ?? "");
5903
+ const [args, setArgs] = (0, react.useState)((editMcp?.args ?? []).join(" "));
5904
+ const [cwd, setCwd] = (0, react.useState)(editMcp?.cwd ?? "");
5905
+ const [env, setEnv] = (0, react.useState)(toLines(editMcp?.env));
5906
+ const [url, setUrl] = (0, react.useState)(editMcp?.url ?? "");
5907
+ const [headers, setHeaders] = (0, react.useState)(toLines(editMcp?.headers));
5908
+ const [timeoutSec, setTimeoutSec] = (0, react.useState)(editMcp?.toolCallTimeoutMs === void 0 ? "" : String(Math.round(editMcp.toolCallTimeoutMs / 1e3)));
5909
+ const [skillDir, setSkillDir] = (0, react.useState)(editSkill?.target ?? editSkill?.path ?? "");
5910
+ const [skillRoot, setSkillRoot] = (0, react.useState)(editSkill?.root ?? "user");
5911
+ /** A path inside the project; the server derives the project root from it. */
5912
+ const [projectPath, setProjectPath] = (0, react.useState)("");
5913
+ /**
5914
+ * What the operator sees this entry called. They got here from a row named by
5915
+ * the skill's declared name, so the messages use that and not the entry's own
5916
+ * name — which is only ever handed back to the server, since that is what
5917
+ * addresses the symlink on disk.
5918
+ */
5919
+ const skillLabel = editSkill?.skillName ?? editSkill?.name ?? "";
5920
+ const title = (0, react.useMemo)(() => {
5921
+ if (editMcp !== void 0) return t("editMcp");
5922
+ if (editSkill !== void 0) return t("editSkillNamed", { name: skillLabel });
5923
+ return t("registerCapability");
5924
+ }, [
5925
+ editMcp,
5926
+ editSkill,
5927
+ skillLabel,
5928
+ t
5929
+ ]);
5930
+ /** Run a mutation; on success close and ask the parent to re-pull. */
5931
+ const submit = (0, react.useCallback)(async (action, noticeFor) => {
5932
+ if (busy) return;
5933
+ setBusy(true);
5934
+ try {
5935
+ const changed = await action();
5936
+ if (changed === false) throw new Error(t("entryNotFound"));
5937
+ setError(null);
5938
+ onChanged(noticeFor?.(changed));
5939
+ onClose();
5940
+ } catch (e) {
5941
+ setError(String(e));
5942
+ } finally {
5943
+ setBusy(false);
5944
+ }
5945
+ }, [
5946
+ busy,
5947
+ onChanged,
5948
+ onClose,
5949
+ t
5950
+ ]);
5951
+ const submitMcp = (0, react.useCallback)(() => {
5952
+ const trimmedTimeout = timeoutSec.trim();
5953
+ const timeoutMs = trimmedTimeout.length === 0 ? void 0 : Math.round(Number(trimmedTimeout) * 1e3);
5954
+ if (timeoutMs !== void 0 && !Number.isFinite(timeoutMs)) {
5955
+ setError(t("timeoutInvalid"));
5956
+ return;
5957
+ }
5958
+ const parsedEnv = fromLines(env);
5959
+ const parsedHeaders = fromLines(headers);
5960
+ const common = {
5961
+ transport,
5962
+ ...transport === "stdio" ? {
5963
+ command: command.trim(),
5964
+ ...args.trim().length > 0 ? { args: args.trim().split(/\s+/) } : {},
5965
+ ...parsedEnv !== void 0 ? { env: parsedEnv } : {},
5966
+ ...cwd.trim().length > 0 ? { cwd: cwd.trim() } : {}
5967
+ } : {
5968
+ url: url.trim(),
5969
+ ...parsedHeaders !== void 0 ? { headers: parsedHeaders } : {}
5970
+ },
5971
+ ...timeoutMs !== void 0 ? { toolCallTimeoutMs: timeoutMs } : {}
5972
+ };
5973
+ submit(async () => editMcp !== void 0 ? unwrapMessage(await remote.updateLocation(editMcp.id, common)) : unwrapMessage(await remote.addLocation({
5974
+ serverName: serverName.trim(),
5975
+ ...common
5976
+ })));
5977
+ }, [
5978
+ submit,
5979
+ remote,
5980
+ editMcp,
5981
+ serverName,
5982
+ transport,
5983
+ command,
5984
+ args,
5985
+ cwd,
5986
+ env,
5987
+ url,
5988
+ headers,
5989
+ timeoutSec,
5990
+ t
5991
+ ]);
5992
+ const submitSkill = (0, react.useCallback)(() => {
5993
+ const asked = skillRoot === "project" ? projectPath.trim() : void 0;
5994
+ submit(async () => editSkill !== void 0 ? unwrapMessage(await remote.updateSkillLocation(editSkill.name, skillDir.trim(), editSkill.entryDir)) : unwrapMessage(await remote.addSkillLocation(skillDir.trim(), asked)), (result) => editSkill !== void 0 ? t("skillRepointed", { dir: skillDir.trim() }) : t("skillRegisteredAt", { path: String(result) }));
5995
+ }, [
5996
+ submit,
5997
+ remote,
5998
+ editSkill,
5999
+ skillDir,
6000
+ skillRoot,
6001
+ projectPath,
6002
+ t
6003
+ ]);
6004
+ const remove = (0, react.useCallback)(() => {
6005
+ if (editMcp !== void 0) {
6006
+ submit(async () => unwrapMessage(await remote.removeLocation(editMcp.id)));
6007
+ return;
6008
+ }
6009
+ if (editSkill !== void 0) submit(async () => unwrapMessage(await remote.removeSkillLocation(editSkill.name, editSkill.entryDir)));
6010
+ }, [
6011
+ submit,
6012
+ remote,
6013
+ editMcp,
6014
+ editSkill
6015
+ ]);
6016
+ /**
6017
+ * What removal actually costs, which differs by target: an MCP row only loses
6018
+ * its config, a linked skill only loses its symlink — but `removeSkill`
6019
+ * recursively deletes a *real* directory in the skill root, so that case has to
6020
+ * say so before the click, not after.
6021
+ */
6022
+ const removalWarning = (0, react.useMemo)(() => {
6023
+ if (editMcp !== void 0) return t("confirmRemoveMcp", { name: editMcp.serverName });
6024
+ if (editSkill === void 0) return "";
6025
+ return editSkill.linked ? t("confirmRemoveSkillLink", { name: skillLabel }) : t("confirmRemoveSkillDir", { name: skillLabel });
6026
+ }, [
6027
+ editMcp,
6028
+ editSkill,
6029
+ skillLabel,
6030
+ t
6031
+ ]);
6032
+ /** Saving needs a confirmation only when it would delete a real directory. */
6033
+ const needsSaveConfirm = editSkill !== void 0 && !editSkill.linked;
6034
+ /** The action row, or the confirmation step that has replaced it. */
6035
+ const renderActions = (onSubmit) => confirm !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6036
+ className: "lm-confirm",
6037
+ role: "alert",
6038
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6039
+ className: "lm-confirm-msg",
6040
+ children: confirm === "save" ? t("saveConfirmRealDir", { name: skillLabel }) : removalWarning
6041
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6042
+ className: "lm-confirm-actions",
6043
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6044
+ type: "button",
6045
+ className: "lm-btn",
6046
+ disabled: busy,
6047
+ onClick: () => setConfirm(null),
6048
+ children: t("cancel")
6049
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6050
+ type: "button",
6051
+ className: "lm-btn lm-btn--danger",
6052
+ disabled: busy,
6053
+ onClick: () => {
6054
+ if (confirm === "save") onSubmit();
6055
+ else remove();
6056
+ },
6057
+ children: confirm === "save" ? t("confirmSaveAnyway") : t("confirmRemove")
6058
+ })]
6059
+ })]
6060
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6061
+ className: "lm-actions",
6062
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6063
+ type: "button",
6064
+ className: "lm-btn",
6065
+ disabled: busy,
6066
+ onClick: () => {
6067
+ if (needsSaveConfirm) setConfirm("save");
6068
+ else onSubmit();
6069
+ },
6070
+ children: editing ? t("save") : t("register")
6071
+ }), editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6072
+ type: "button",
6073
+ className: "lm-btn lm-btn--danger",
6074
+ disabled: busy,
6075
+ onClick: () => setConfirm("remove"),
6076
+ children: t("remove")
6077
+ })]
6078
+ });
6079
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6080
+ className: "mc-preview-mask",
6081
+ onClick: onClose,
6082
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6083
+ className: "mc-preview",
6084
+ onClick: (e) => e.stopPropagation(),
6085
+ children: [
6086
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6087
+ className: "mc-preview-head",
6088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6089
+ className: "mc-preview-title",
6090
+ children: title
6091
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6092
+ type: "button",
6093
+ className: "mc-preview-close",
6094
+ onClick: onClose,
6095
+ children: t("previewClose")
6096
+ })]
6097
+ }),
6098
+ !editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6099
+ className: "mc-subtabs mc-catalog-tabs",
6100
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6101
+ className: "mc-tab-group",
6102
+ role: "tablist",
6103
+ "aria-label": t("registerCapability"),
6104
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6105
+ type: "button",
6106
+ role: "tab",
6107
+ className: "mc-tab",
6108
+ "aria-selected": subTab === "mcp",
6109
+ "data-active": subTab === "mcp" ? "true" : void 0,
6110
+ onClick: () => setSubTab("mcp"),
6111
+ children: t("mcpServers")
6112
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6113
+ type: "button",
6114
+ role: "tab",
6115
+ className: "mc-tab",
6116
+ "aria-selected": subTab === "skill",
6117
+ "data-active": subTab === "skill" ? "true" : void 0,
6118
+ onClick: () => setSubTab("skill"),
6119
+ children: t("skillDirs")
6120
+ })]
6121
+ })
6122
+ }),
6123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6124
+ className: "lm-body",
6125
+ children: [error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6126
+ className: "lm-error",
6127
+ children: error
6128
+ }), subTab === "mcp" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6129
+ className: "lm-block",
6130
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6131
+ className: "lm-form",
6132
+ children: [
6133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6134
+ className: "lm-field",
6135
+ children: [
6136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6137
+ className: "lm-label",
6138
+ children: t("transport")
6139
+ }),
6140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6141
+ className: "lm-input",
6142
+ value: transport,
6143
+ onChange: (e) => setTransport(e.target.value === "stdio" ? "stdio" : "streamable-http"),
6144
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6145
+ value: "stdio",
6146
+ children: t("transportStdio")
6147
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6148
+ value: "streamable-http",
6149
+ children: t("transportHttp")
6150
+ })]
6151
+ }),
6152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6153
+ className: "lm-hint",
6154
+ children: transport === "stdio" ? t("transportHintStdio") : t("transportHintHttp")
6155
+ })
6156
+ ]
6157
+ }),
6158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6159
+ className: "lm-field",
6160
+ children: [
6161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6162
+ className: "lm-label",
6163
+ children: t("serverName")
6164
+ }),
6165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6166
+ className: "lm-input",
6167
+ value: serverName,
6168
+ disabled: editMcp !== void 0,
6169
+ onChange: (e) => setServerName(e.target.value)
6170
+ }),
6171
+ editMcp !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6172
+ className: "lm-hint",
6173
+ children: t("serverNameImmutable")
6174
+ })
6175
+ ]
6176
+ }),
6177
+ transport === "stdio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
6178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6179
+ className: "lm-field",
6180
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6181
+ className: "lm-label",
6182
+ children: t("command")
6183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6184
+ className: "lm-input",
6185
+ value: command,
6186
+ onChange: (e) => setCommand(e.target.value)
6187
+ })]
6188
+ }),
6189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6190
+ className: "lm-field",
6191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6192
+ className: "lm-label",
6193
+ children: t("args")
6194
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6195
+ className: "lm-input",
6196
+ value: args,
6197
+ onChange: (e) => setArgs(e.target.value)
6198
+ })]
6199
+ }),
6200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6201
+ className: "lm-field",
6202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6203
+ className: "lm-label",
6204
+ children: t("cwd")
6205
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6206
+ className: "lm-input",
6207
+ value: cwd,
6208
+ onChange: (e) => setCwd(e.target.value)
6209
+ })]
6210
+ }),
6211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6212
+ className: "lm-field",
6213
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6214
+ className: "lm-label",
6215
+ children: t("env")
6216
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6217
+ className: "lm-input",
6218
+ rows: 3,
6219
+ value: env,
6220
+ onChange: (e) => setEnv(e.target.value)
6221
+ })]
6222
+ })
6223
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6224
+ className: "lm-field",
6225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6226
+ className: "lm-label",
6227
+ children: t("url")
6228
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6229
+ className: "lm-input",
6230
+ value: url,
6231
+ onChange: (e) => setUrl(e.target.value)
6232
+ })]
6233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6234
+ className: "lm-field",
6235
+ children: [
6236
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6237
+ className: "lm-label",
6238
+ children: t("headers")
6239
+ }),
6240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6241
+ className: "lm-input",
6242
+ rows: 4,
6243
+ value: headers,
6244
+ onChange: (e) => setHeaders(e.target.value)
6245
+ }),
6246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6247
+ className: "lm-hint",
6248
+ children: t("headersHint")
6249
+ })
6250
+ ]
6251
+ })] }),
6252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6253
+ className: "lm-field",
6254
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6255
+ className: "lm-label",
6256
+ children: t("timeout")
6257
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6258
+ className: "lm-input",
6259
+ value: timeoutSec,
6260
+ placeholder: "60",
6261
+ onChange: (e) => setTimeoutSec(e.target.value)
6262
+ })]
6263
+ }),
6264
+ renderActions(() => void submitMcp())
6265
+ ]
6266
+ })
6267
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6268
+ className: "lm-block",
6269
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6270
+ className: "lm-form",
6271
+ children: [
6272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6273
+ className: "lm-field",
6274
+ children: [
6275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6276
+ className: "lm-label",
6277
+ children: t("skillRoot")
6278
+ }),
6279
+ editSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6280
+ className: "lm-input",
6281
+ value: t(editSkill.root === "project" ? "skillRootProject" : "skillRootUser"),
6282
+ disabled: true
6283
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6284
+ className: "lm-input",
6285
+ value: skillRoot,
6286
+ onChange: (e) => setSkillRoot(e.target.value === "project" ? "project" : "user"),
6287
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6288
+ value: "user",
6289
+ children: t("skillRootUser")
6290
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6291
+ value: "project",
6292
+ children: t("skillRootProject")
6293
+ })]
6294
+ }),
6295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6296
+ className: "lm-hint",
6297
+ children: skillRoot === "project" || editSkill?.root === "project" ? t("skillRootHintProject") : t("skillRootHintUser")
6298
+ })
6299
+ ]
6300
+ }),
6301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6302
+ className: "lm-field",
6303
+ children: [
6304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6305
+ className: "lm-label",
6306
+ children: t("skillDirPath")
6307
+ }),
6308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6309
+ className: "lm-input",
6310
+ value: skillDir,
6311
+ onChange: (e) => setSkillDir(e.target.value)
6312
+ }),
6313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6314
+ className: "lm-hint",
6315
+ children: t("skillDirHint")
6316
+ })
6317
+ ]
6318
+ }),
6319
+ editSkill === void 0 && skillRoot === "project" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6320
+ className: "lm-field",
6321
+ children: [
6322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6323
+ className: "lm-label",
6324
+ children: t("skillProjectPath")
6325
+ }),
6326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6327
+ className: "lm-input",
6328
+ value: projectPath,
6329
+ onChange: (e) => setProjectPath(e.target.value)
6330
+ }),
6331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6332
+ className: "lm-hint",
6333
+ children: t("skillProjectPathHint")
6334
+ })
6335
+ ]
6336
+ }),
6337
+ renderActions(() => void submitSkill())
6338
+ ]
6339
+ })
6340
+ })]
6341
+ })
6342
+ ]
6343
+ })
6344
+ });
5439
6345
  }
6346
+ /**
6347
+ * dsh's source labels for a project's own skill roots — the `project`-prefixed
6348
+ * providers of `@deepseek-ai/dsh-skill-filesystem`, which scan
6349
+ * `<projectRoot>/.dsh/skills` and `<projectRoot>/.agents/skills`. Every other
6350
+ * label (`user-dsh`, `user-agents`, `bundled`, `custom`, `runtime`) is a
6351
+ * user-level root. Shared so the server's location manager and the browser's
6352
+ * 全局 / 项目 grouping agree on what "project scoped" means.
6353
+ */
6354
+ const PROJECT_SKILL_SOURCES = /* @__PURE__ */ new Set(["project-dsh", "project-agents"]);
6355
+ /**
6356
+ * Source labels for user-level skill roots whose entries this plugin can adopt:
6357
+ * linking the skill's own directory into the user root (where the entry becomes
6358
+ * editable) without touching its content. `user-dsh` is excluded because that
6359
+ * root is already managed, and `bundled` because a packaged skill should not be
6360
+ * shadowed by a user copy.
6361
+ */
6362
+ const ADOPTABLE_SKILL_SOURCES = /* @__PURE__ */ new Set(["user-agents", "custom"]);
5440
6363
  //#endregion
5441
6364
  //#region src/client/CapabilitySection.tsx
5442
6365
  /**
5443
6366
  * ⚠️ VERIFIED AGAINST REAL rc.8 CLIENT API.
5444
6367
  *
5445
- * React section for the 能力管理 settings tab. Follows the real dsh client
6368
+ * React section for the 能力菜单 settings tab. Follows the real dsh client
5446
6369
  * pattern (see `dsh-client-ui-settings-plugins`): two tabs (工具 / Skills)
5447
6370
  * under one heading, each listing capabilities with a clickable class chip.
5448
6371
  *
@@ -5482,6 +6405,10 @@ const CSS = `
5482
6405
  .mc-section{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary)}
5483
6406
  .mc-heading{margin:0;font-size:18px;font-weight:600}
5484
6407
  .mc-desc{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px}
6408
+ /* 说明行右侧放只读文档入口:把它从头部的计数行挪出来,计数多(Tools 常驻 ·
6409
+ 167)时那一行不再因此折行。 */
6410
+ .mc-desc-row{display:flex;align-items:center;justify-content:space-between;gap:12px}
6411
+ .mc-desc-row .mc-desc{flex:1 1 auto;min-width:0}
5485
6412
  .mc-summary{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end;align-items:center;padding-bottom:8px}
5486
6413
  .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
6414
  .mc-catalog-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
@@ -5516,6 +6443,14 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
5516
6443
  .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
6444
  .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
6445
  .mc-panel{min-width:0;padding-top:12px}
6446
+ /* Skills panel: its first row is the 全局技能/项目技能 sub-tab bar, which sits
6447
+ directly under the main tab bar. It is a second level of the same control, so
6448
+ the two bars have to read as one stack: no panel padding at all, and the
6449
+ sub-tabs keep a token top padding instead of the 7px they share with the
6450
+ primary tabs. Together the label sits ~13px under the main label rather than
6451
+ ~20px, with the main bar's rule between them. */
6452
+ .mc-panel--tight{padding-top:0}
6453
+ .mc-panel--tight .mc-subtabs .mc-tab{padding-top:4px}
5519
6454
  .mc-panel-inner{display:flex;flex-direction:column;gap:14px}
5520
6455
  .mc-group{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}
5521
6456
  .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 +6471,11 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
5536
6471
  body[data-ds-dark-theme] .mc-count--resident{color:#96b6d1}
5537
6472
  body[data-ds-dark-theme] .mc-count--on-demand{color:#d4b26b}
5538
6473
  body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
6474
+ /* 「编辑」是动作,不是分类计数:沿用 chip 的尺寸以对齐,但用次要文字色、hover
6475
+ 才变蓝加下划线。裸用 .mc-count 时会继承 .mc-section 的 label-primary(浅色
6476
+ 主题下近黑),夹在着色的计数 chip 旁会读成一枚没有圆点的黑色 chip。 */
6477
+ .mc-count--action{color:var(--dsw-alias-label-secondary)}
6478
+ .mc-count--action:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}
5539
6479
  .mc-tools{border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base)}
5540
6480
  .mc-tool{display:flex;align-items:center;gap:10px;padding:7px 12px 7px 26px;font-size:13px;line-height:20px;cursor:pointer}
5541
6481
  .mc-tool:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -5547,13 +6487,26 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
5547
6487
  .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
6488
  .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
6489
  .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)}
6490
+ .mc-error p{margin:0}
6491
+ .mc-error p.mc-error-hint{margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
6492
+ .mc-error-actions{display:flex;justify-content:flex-end;margin-top:10px}
5551
6493
  .mc-notice{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:13px}
5552
6494
  .mc-skill{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
5553
6495
  .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
6496
  .mc-skill-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
5555
6497
  .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
6498
  .mc-skill-meta{margin-left:auto;display:flex;align-items:center;gap:8px;flex:0 1 auto;min-width:0}
6499
+ /* A row has no room for prose: let a long source label truncate instead of
6500
+ pushing the row's buttons past the right edge. */
6501
+ .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}
6502
+ /* One sentence to confirm a row action: the shared modal chrome, sized down. */
6503
+ .mc-confirm-dialog{width:min(420px,calc(100vw - 48px))}
6504
+ .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)}
6505
+ .mc-confirm-body p{margin:0}
6506
+ /* A directory has no spaces to break at, and the modal is where it can show in
6507
+ full: wrap it instead of truncating it the way a row's 来源 slot must. */
6508
+ .mc-confirm-path{font-family:var(--dsw-font-markdown-code-block-font-family);word-break:break-all}
6509
+ .mc-confirm-actions{display:flex;justify-content:flex-end;gap:8px}
5557
6510
  .mc-skill-body{border-top:1px solid var(--dsw-alias-border-l1);padding:8px 12px 12px}
5558
6511
  .mc-tree{display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:20px}
5559
6512
  .mc-tree-row{display:flex;align-items:center;gap:8px;padding:3px 4px;border-radius:6px;cursor:pointer;min-width:0}
@@ -5577,10 +6530,31 @@ if (typeof document !== "undefined" && document.querySelector(`style[data-css-id
5577
6530
  tag.textContent = CSS;
5578
6531
  document.head.appendChild(tag);
5579
6532
  }
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"]);
6533
+ /**
6534
+ * dsh's skill source labels, shown on rows this plugin cannot edit — so "why is
6535
+ * there no 编辑 button" has a visible answer. Labels that name one directory are
6536
+ * shown as that path; the rest name a category and go through the dictionary, so
6537
+ * none of dsh's internal labels (`custom`, `bundled`, …) reach the operator raw.
6538
+ */
6539
+ const SOURCE_PATHS = {
6540
+ "user-dsh": "~/.dsh/skills",
6541
+ "user-agents": "~/.agents/skills",
6542
+ "project-dsh": "<project>/.dsh/skills",
6543
+ "project-agents": "<project>/.agents/skills"
6544
+ };
6545
+ /** Source labels that name no single directory; shown via `t`. */
6546
+ const SOURCE_LABEL_KEYS = {
6547
+ "custom": "sourceCustom",
6548
+ "bundled": "sourceBundled"
6549
+ };
6550
+ /** How to name a skill's source root on a row we cannot manage. */
6551
+ function sourceLabel(source, t) {
6552
+ if (source === void 0) return t("skillUnmanaged");
6553
+ const path = SOURCE_PATHS[source];
6554
+ if (path !== void 0) return path;
6555
+ const key = SOURCE_LABEL_KEYS[source];
6556
+ return key === void 0 ? source : t(key);
6557
+ }
5584
6558
  function groupRows(rows) {
5585
6559
  const byServer = /* @__PURE__ */ new Map();
5586
6560
  const skills = [];
@@ -5589,7 +6563,7 @@ function groupRows(rows) {
5589
6563
  skills.push(row);
5590
6564
  continue;
5591
6565
  }
5592
- const server = row.server ?? BUILT_IN_SERVER;
6566
+ const server = row.server ?? "built-in";
5593
6567
  const list = byServer.get(server);
5594
6568
  if (list === void 0) byServer.set(server, [row]);
5595
6569
  else list.push(row);
@@ -5606,8 +6580,14 @@ function countByClass(rows, cls) {
5606
6580
  return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
5607
6581
  }
5608
6582
  function CapabilitySection(props) {
5609
- const { remote, t, mountError, remoteKeys } = props;
5610
- const [state, setState] = (0, react.useState)({ status: "loading" });
6583
+ const { remote, t, mountError } = props;
6584
+ const [state, setState] = (0, react.useState)(() => {
6585
+ const cached = cachedSnapshot();
6586
+ return cached === void 0 ? { status: "loading" } : {
6587
+ status: "ready",
6588
+ snapshot: cached
6589
+ };
6590
+ });
5611
6591
  const [busy, setBusy] = (0, react.useState)(false);
5612
6592
  const [notice, setNotice] = (0, react.useState)(null);
5613
6593
  const [openServers, setOpenServers] = (0, react.useState)(/* @__PURE__ */ new Set());
@@ -5626,15 +6606,41 @@ function CapabilitySection(props) {
5626
6606
  snapshot
5627
6607
  })));
5628
6608
  } catch (e) {
5629
- setState({
6609
+ setState((prev) => prev.status === "ready" ? prev : {
5630
6610
  status: "error",
5631
6611
  message: String(e)
5632
6612
  });
6613
+ setNotice(String(e));
5633
6614
  }
5634
6615
  }, [remote, mountError]);
5635
6616
  (0, react.useEffect)(() => {
5636
6617
  reload();
5637
6618
  }, [reload]);
6619
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
6620
+ /**
6621
+ * Rebuild the host catalog, then re-read the classification list. Used after
6622
+ * registering a new source so the list reflects it without waiting for the
6623
+ * scheduler's debounce window.
6624
+ */
6625
+ const refreshCatalog = (0, react.useCallback)(async () => {
6626
+ if (refreshing) return;
6627
+ setRefreshing(true);
6628
+ try {
6629
+ if (remote !== void 0) unwrap(await remote.refresh(), "capabilityPolicy.refresh");
6630
+ await reload();
6631
+ setNotice(null);
6632
+ } catch (e) {
6633
+ console.error("[capability-menu] refresh failed:", e);
6634
+ setNotice(t("refreshFailed"));
6635
+ } finally {
6636
+ setRefreshing(false);
6637
+ }
6638
+ }, [
6639
+ refreshing,
6640
+ remote,
6641
+ reload,
6642
+ t
6643
+ ]);
5638
6644
  const toggleServer = (0, react.useCallback)((server) => {
5639
6645
  setOpenServers((prev) => {
5640
6646
  const next = new Set(prev);
@@ -5647,6 +6653,17 @@ function CapabilitySection(props) {
5647
6653
  const cycleClass = (0, react.useCallback)(async (ids, kind) => {
5648
6654
  if (ids.length === 0 || busy) return;
5649
6655
  setBusy(true);
6656
+ 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";
6657
+ const to = NEXT_CLASS[from];
6658
+ const moved = new Set(ids);
6659
+ setState((prev) => prev.status === "ready" ? {
6660
+ status: "ready",
6661
+ snapshot: { rows: prev.snapshot.rows.map((row) => moved.has(row.id) ? {
6662
+ ...row,
6663
+ class: to,
6664
+ classLabel: void 0
6665
+ } : row) }
6666
+ } : prev);
5650
6667
  try {
5651
6668
  const config = unwrap(await remote.getConfig(), "capabilityPolicy.getConfig");
5652
6669
  const key = kind === "skill" ? "skills" : "tools";
@@ -5660,13 +6677,11 @@ function CapabilitySection(props) {
5660
6677
  const raw = set && typeof set === "object" && !Array.isArray(set) ? set[cls] : void 0;
5661
6678
  lists[cls] = Array.isArray(raw) ? raw.filter((x) => typeof x === "string") : [];
5662
6679
  }
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
6680
  for (const cls of CLASS_KEYS) lists[cls] = lists[cls].filter((id) => !ids.includes(id));
5666
6681
  lists[to] = [...lists[to], ...ids];
5667
6682
  const nextLists = {};
5668
6683
  for (const cls of CLASS_KEYS) nextLists[cls] = lists[cls];
5669
- await remote.updateConfig({ [key]: nextLists });
6684
+ unwrap(await remote.updateConfig({ [key]: nextLists }), "capabilityPolicy.updateConfig");
5670
6685
  const next = await loadSnapshot(remote);
5671
6686
  setState({
5672
6687
  status: "ready",
@@ -5697,7 +6712,22 @@ function CapabilitySection(props) {
5697
6712
  }),
5698
6713
  state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5699
6714
  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)}` })]
6715
+ children: [
6716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: state.message }),
6717
+ state.message.includes("Failed to fetch") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6718
+ className: "mc-error-hint",
6719
+ children: t("carrierFailureHint")
6720
+ }),
6721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6722
+ className: "mc-error-actions",
6723
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6724
+ type: "button",
6725
+ className: "mc-catalog-btn",
6726
+ onClick: () => void reload(),
6727
+ children: t("retry")
6728
+ })
6729
+ })
6730
+ ]
5701
6731
  }),
5702
6732
  notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5703
6733
  className: "mc-notice",
@@ -5712,16 +6742,19 @@ function CapabilitySection(props) {
5712
6742
  t,
5713
6743
  onTabChange: setActiveTab,
5714
6744
  onToggleServer: toggleServer,
5715
- onCycle: cycleClass
6745
+ onCycle: cycleClass,
6746
+ onRefresh: () => void refreshCatalog(),
6747
+ refreshing,
6748
+ onNotice: setNotice
5716
6749
  })
5717
6750
  ]
5718
6751
  });
5719
6752
  }
5720
6753
  function ReadyBody(props) {
5721
- const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle } = props;
6754
+ const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh, refreshing, onNotice } = props;
5722
6755
  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 ?? ""));
6756
+ const projectSkills = skills.filter((skill) => PROJECT_SKILL_SOURCES.has(skill.source ?? ""));
6757
+ const globalSkills = skills.filter((skill) => !PROJECT_SKILL_SOURCES.has(skill.source ?? ""));
5725
6758
  const [skillTab, setSkillTab] = (0, react.useState)("global");
5726
6759
  const statRows = activeTab === "tools" ? snapshot.rows.filter((r) => r.kind === "tool") : skillTab === "project" ? projectSkills : globalSkills;
5727
6760
  const summary = CLASS_KEYS.map((cls) => ({
@@ -5758,6 +6791,93 @@ function ReadyBody(props) {
5758
6791
  /** 能力目录弹层状态:查看三档策略配置 + 按需能力目录文件。 */
5759
6792
  const [catalogDocs, setCatalogDocs] = (0, react.useState)(null);
5760
6793
  const [catalogTab, setCatalogTab] = (0, react.useState)("policy");
6794
+ /** 注册能力 modal; its default sub-tab follows `activeTab`. */
6795
+ const [registerOpen, setRegisterOpen] = (0, react.useState)(false);
6796
+ /** 编辑 target; at most one is set. */
6797
+ const [editMcp, setEditMcp] = (0, react.useState)(void 0);
6798
+ const [editSkill, setEditSkill] = (0, react.useState)(void 0);
6799
+ /**
6800
+ * Skill directories registered under the skill root. Only those can be
6801
+ * edited: a project or bundled skill has no row to repoint, so its row gets
6802
+ * no 编辑 button rather than a button that always fails.
6803
+ */
6804
+ const [editableSkills, setEditableSkills] = (0, react.useState)(/* @__PURE__ */ new Set());
6805
+ const skillSignature = skills.map((skill) => `${skill.id}\u0000${skill.source ?? ""}`).join("");
6806
+ (0, react.useEffect)(() => {
6807
+ let cancelled = false;
6808
+ (async () => {
6809
+ try {
6810
+ const rows = unwrap(await remote.listSkillLocations(), "capabilityPolicy.listSkillLocations");
6811
+ if (!cancelled) setEditableSkills(new Set(rows.map((row) => row.skillName ?? row.name)));
6812
+ } catch {}
6813
+ })();
6814
+ return () => {
6815
+ cancelled = true;
6816
+ };
6817
+ }, [remote, skillSignature]);
6818
+ /** Open the 编辑 form for the MCP server behind a Tools-tab group header. */
6819
+ const openMcpEdit = (0, react.useCallback)(async (serverName) => {
6820
+ try {
6821
+ const found = unwrap(await remote.listLocations(), "capabilityPolicy.listLocations").find((row) => row.serverName === serverName);
6822
+ if (found === void 0) {
6823
+ onNotice(t("notEditable"));
6824
+ return;
6825
+ }
6826
+ setEditMcp(found);
6827
+ } catch (e) {
6828
+ onNotice(String(e));
6829
+ }
6830
+ }, [
6831
+ remote,
6832
+ t,
6833
+ onNotice
6834
+ ]);
6835
+ /**
6836
+ * Adopt a skill that dsh discovered from a user-level root this plugin reads
6837
+ * but does not manage: link its own directory into the user root so it becomes
6838
+ * an editable entry. The content is untouched.
6839
+ */
6840
+ const adoptSkill = (0, react.useCallback)(async (name) => {
6841
+ try {
6842
+ const entry = unwrap(await remote.adoptSkillLocation(name), "capabilityPolicy.adoptSkillLocation");
6843
+ onNotice(t("skillAdopted", { path: entry }));
6844
+ onRefresh();
6845
+ } catch (e) {
6846
+ onNotice(String(e));
6847
+ }
6848
+ }, [
6849
+ remote,
6850
+ t,
6851
+ onNotice,
6852
+ onRefresh
6853
+ ]);
6854
+ /**
6855
+ * Open the 编辑 form for a managed skill entry. `name` is the skill's declared
6856
+ * name, which is what the row and the panel address it by — an entry is
6857
+ * matched on that rather than on its own, possibly different, name. A bare
6858
+ * name can exist in both the user root and a project root, so the row's source
6859
+ * decides which entry the click meant; without it we fall back to the first
6860
+ * match.
6861
+ */
6862
+ const openSkillEdit = (0, react.useCallback)(async (name, source) => {
6863
+ try {
6864
+ const rows = unwrap(await remote.listSkillLocations(), "capabilityPolicy.listSkillLocations");
6865
+ const wanted = PROJECT_SKILL_SOURCES.has(source ?? "") ? "project" : "user";
6866
+ const named = rows.filter((row) => (row.skillName ?? row.name) === name);
6867
+ const found = named.find((row) => row.root === wanted) ?? named[0];
6868
+ if (found === void 0) {
6869
+ onNotice(t("notEditable"));
6870
+ return;
6871
+ }
6872
+ setEditSkill(found);
6873
+ } catch (e) {
6874
+ onNotice(String(e));
6875
+ }
6876
+ }, [
6877
+ remote,
6878
+ t,
6879
+ onNotice
6880
+ ]);
5761
6881
  /** Fetch and show the two read-only catalog documents. */
5762
6882
  const openCatalogDocs = (0, react.useCallback)(async () => {
5763
6883
  setCatalogDocs({ status: "loading" });
@@ -5779,9 +6899,17 @@ function ReadyBody(props) {
5779
6899
  className: "mc-heading",
5780
6900
  children: t("title")
5781
6901
  }),
5782
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5783
- className: "mc-desc",
5784
- children: t("desc")
6902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6903
+ className: "mc-desc-row",
6904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6905
+ className: "mc-desc",
6906
+ children: t("desc")
6907
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6908
+ type: "button",
6909
+ className: "mc-catalog-btn",
6910
+ onClick: () => void openCatalogDocs(),
6911
+ children: t("viewCatalog")
6912
+ })]
5785
6913
  }),
5786
6914
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5787
6915
  className: "mc-tabs",
@@ -5808,23 +6936,33 @@ function ReadyBody(props) {
5808
6936
  })]
5809
6937
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5810
6938
  className: "mc-summary",
5811
- children: [summary.map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5812
- className: `mc-chip mc-chip--${cls}`,
5813
- children: [
5814
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5815
- className: `mc-dot mc-dot--${cls}`,
5816
- "aria-hidden": "true"
5817
- }),
5818
- t(CLASS_SHORT_KEYS[cls]),
5819
- " · ",
5820
- count
5821
- ]
5822
- }, cls)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5823
- type: "button",
5824
- className: "mc-catalog-btn",
5825
- onClick: () => void openCatalogDocs(),
5826
- children: t("viewCatalog")
5827
- })]
6939
+ children: [
6940
+ summary.map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6941
+ className: `mc-chip mc-chip--${cls}`,
6942
+ children: [
6943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6944
+ className: `mc-dot mc-dot--${cls}`,
6945
+ "aria-hidden": "true"
6946
+ }),
6947
+ t(CLASS_SHORT_KEYS[cls]),
6948
+ " · ",
6949
+ count
6950
+ ]
6951
+ }, cls)),
6952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6953
+ type: "button",
6954
+ className: "mc-catalog-btn",
6955
+ onClick: () => onRefresh(),
6956
+ disabled: refreshing,
6957
+ children: refreshing ? t("refreshing") : t("refresh")
6958
+ }),
6959
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6960
+ type: "button",
6961
+ className: "mc-catalog-btn",
6962
+ onClick: () => setRegisterOpen(true),
6963
+ children: t("registerCapability")
6964
+ })
6965
+ ]
5828
6966
  })]
5829
6967
  }),
5830
6968
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5863,15 +7001,15 @@ function ReadyBody(props) {
5863
7001
  }),
5864
7002
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5865
7003
  className: "mc-server-name",
5866
- children: server === BUILT_IN_SERVER ? t("builtInGroup") : server
7004
+ children: server === "built-in" ? t("builtInGroup") : server
5867
7005
  }),
5868
7006
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5869
7007
  className: "mc-server-count",
5870
7008
  children: t("toolCount", { count: tools.length })
5871
7009
  }),
5872
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5873
7011
  className: "mc-server-meta",
5874
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7012
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5875
7013
  className: "mc-counts",
5876
7014
  children: counts.filter(({ count }) => count > 0).map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5877
7015
  type: "button",
@@ -5892,7 +7030,16 @@ function ReadyBody(props) {
5892
7030
  count
5893
7031
  ]
5894
7032
  }, cls))
5895
- })
7033
+ }), server !== "built-in" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7034
+ type: "button",
7035
+ className: "mc-count mc-count--action",
7036
+ disabled: busy,
7037
+ onClick: (e) => {
7038
+ e.stopPropagation();
7039
+ openMcpEdit(server);
7040
+ },
7041
+ children: t("edit")
7042
+ })]
5896
7043
  })
5897
7044
  ]
5898
7045
  }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5943,7 +7090,7 @@ function ReadyBody(props) {
5943
7090
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5944
7091
  role: "tabpanel",
5945
7092
  hidden: activeTab !== "skills",
5946
- className: "mc-panel",
7093
+ className: "mc-panel mc-panel--tight",
5947
7094
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5948
7095
  className: "mc-panel-inner",
5949
7096
  children: skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5978,7 +7125,10 @@ function ReadyBody(props) {
5978
7125
  remote,
5979
7126
  busy,
5980
7127
  t,
5981
- onCycle
7128
+ onCycle,
7129
+ editableSkills,
7130
+ onEditSkill: openSkillEdit,
7131
+ onAdoptSkill: adoptSkill
5982
7132
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5983
7133
  className: "mc-empty",
5984
7134
  children: t("emptyGlobalSkills")
@@ -5987,7 +7137,10 @@ function ReadyBody(props) {
5987
7137
  remote,
5988
7138
  busy,
5989
7139
  t,
5990
- onCycle
7140
+ onCycle,
7141
+ editableSkills,
7142
+ onEditSkill: openSkillEdit,
7143
+ onAdoptSkill: adoptSkill
5991
7144
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5992
7145
  className: "mc-empty",
5993
7146
  children: t("emptyProjectSkills")
@@ -6035,6 +7188,22 @@ function ReadyBody(props) {
6035
7188
  ]
6036
7189
  })
6037
7190
  }),
7191
+ (registerOpen || editMcp !== void 0 || editSkill !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocationModal, {
7192
+ remote,
7193
+ t,
7194
+ defaultKind: activeTab === "skills" ? "skill" : "mcp",
7195
+ ...editMcp !== void 0 ? { editMcp } : {},
7196
+ ...editSkill !== void 0 ? { editSkill } : {},
7197
+ onClose: () => {
7198
+ setRegisterOpen(false);
7199
+ setEditMcp(void 0);
7200
+ setEditSkill(void 0);
7201
+ },
7202
+ onChanged: (notice) => {
7203
+ onRefresh();
7204
+ if (notice !== void 0) onNotice(notice);
7205
+ }
7206
+ }),
6038
7207
  catalogDocs !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6039
7208
  className: "mc-preview-mask",
6040
7209
  onClick: () => setCatalogDocs(null),
@@ -6108,7 +7277,9 @@ function ReadyBody(props) {
6108
7277
  ] });
6109
7278
  }
6110
7279
  function SkillList(props) {
6111
- const { skills, remote, busy, t, onCycle } = props;
7280
+ const { skills, remote, busy, t, onCycle, editableSkills, onEditSkill, onAdoptSkill } = props;
7281
+ /** Skill id whose 纳入管理 is waiting for a confirmation click. */
7282
+ const [confirmAdopt, setConfirmAdopt] = (0, react.useState)(null);
6112
7283
  const [openSkill, setOpenSkill] = (0, react.useState)(null);
6113
7284
  const [tree, setTree] = (0, react.useState)({
6114
7285
  open: {},
@@ -6277,89 +7448,165 @@ function SkillList(props) {
6277
7448
  }, relPath);
6278
7449
  });
6279
7450
  };
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
- })]
7451
+ /** The row the 纳入管理 confirmation is for, so the dialog can name it and
7452
+ show the directory the click will link. */
7453
+ const confirmAdoptRow = confirmAdopt === null ? void 0 : skills.find((row) => row.id === confirmAdopt);
7454
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7455
+ skills.map((skill) => {
7456
+ const open = openSkill === skill.id;
7457
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7458
+ className: "mc-skill",
7459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7460
+ className: "mc-skill-row",
7461
+ role: "button",
7462
+ tabIndex: 0,
7463
+ "aria-expanded": open,
7464
+ onClick: () => void toggleSkill(skill.id),
7465
+ onKeyDown: (e) => {
7466
+ if (e.key === "Enter" || e.key === " ") {
7467
+ e.preventDefault();
7468
+ toggleSkill(skill.id);
7469
+ }
7470
+ },
7471
+ children: [
7472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, {
7473
+ size: 12,
7474
+ className: `mc-chevron${open ? " mc-chevron--open" : ""}`
7475
+ }),
7476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7477
+ className: "mc-skill-name",
7478
+ children: skill.name
7479
+ }),
7480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7481
+ className: "mc-skill-meta",
7482
+ children: [
7483
+ skill.mandatory && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7484
+ className: "mc-tag",
7485
+ children: t("mandatory")
7486
+ }),
7487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7488
+ type: "button",
7489
+ className: `mc-count mc-count--${skill.class}`,
7490
+ disabled: busy || skill.mandatory,
7491
+ title: skill.classLabel,
7492
+ onClick: (e) => {
7493
+ e.stopPropagation();
7494
+ onCycle([skill.id], "skill");
7495
+ },
7496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7497
+ className: `mc-dot mc-dot--${skill.class}`,
7498
+ "aria-hidden": "true"
7499
+ }), t(CLASS_SHORT_KEYS[skill.class])]
7500
+ }),
7501
+ !editableSkills.has(skill.id) && (ADOPTABLE_SKILL_SOURCES.has(skill.source ?? "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7502
+ type: "button",
7503
+ className: "mc-count mc-count--action",
7504
+ disabled: busy,
7505
+ title: t("adoptSkillHint"),
7506
+ onClick: (e) => {
7507
+ e.stopPropagation();
7508
+ setConfirmAdopt(skill.id);
7509
+ },
7510
+ children: t("adoptSkill")
7511
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7512
+ className: "mc-source",
7513
+ title: t("skillSourceHint"),
7514
+ children: t("skillSource", { source: sourceLabel(skill.source, t) })
7515
+ })),
7516
+ editableSkills.has(skill.id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7517
+ type: "button",
7518
+ className: "mc-count mc-count--action",
7519
+ disabled: busy,
7520
+ onClick: (e) => {
7521
+ e.stopPropagation();
7522
+ onEditSkill(skill.id, skill.source);
7523
+ },
7524
+ children: t("edit")
7525
+ })
7526
+ ]
7527
+ })
7528
+ ]
7529
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7530
+ className: "mc-skill-body",
7531
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7532
+ className: "mc-tree",
7533
+ children: renderEntries(tree.dirs[`${skill.id}:`], skill.id, "") ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7534
+ className: "mc-tree-row",
7535
+ children: "…"
7536
+ })
6324
7537
  })
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: "…"
7538
+ })]
7539
+ }, skill.id);
7540
+ }),
7541
+ confirmAdopt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7542
+ className: "mc-preview-mask",
7543
+ onClick: () => setConfirmAdopt(null),
7544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7545
+ className: "mc-preview mc-confirm-dialog",
7546
+ onClick: (e) => e.stopPropagation(),
7547
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7548
+ className: "mc-preview-head",
7549
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7550
+ className: "mc-preview-title",
7551
+ children: t("adoptSkillTitle", { name: confirmAdoptRow?.name ?? confirmAdopt })
6333
7552
  })
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")
7553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7554
+ className: "mc-confirm-body",
7555
+ children: [
7556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("adoptSkillHint") }),
7557
+ confirmAdoptRow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7558
+ className: "mc-confirm-path",
7559
+ children: t("skillSource", { source: confirmAdoptRow.path ?? sourceLabel(confirmAdoptRow.source, t) })
7560
+ }),
7561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7562
+ className: "mc-confirm-actions",
7563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7564
+ type: "button",
7565
+ className: "mc-catalog-btn",
7566
+ onClick: () => setConfirmAdopt(null),
7567
+ children: t("cancel")
7568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7569
+ type: "button",
7570
+ className: "mc-catalog-btn",
7571
+ onClick: () => {
7572
+ const id = confirmAdopt;
7573
+ setConfirmAdopt(null);
7574
+ onAdoptSkill(id);
7575
+ },
7576
+ children: t("adoptSkill")
7577
+ })]
7578
+ })
7579
+ ]
6353
7580
  })]
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
- })]
7581
+ })
7582
+ }),
7583
+ preview !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7584
+ className: "mc-preview-mask",
7585
+ onClick: () => setPreview(null),
7586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7587
+ className: "mc-preview",
7588
+ onClick: (e) => e.stopPropagation(),
7589
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7590
+ className: "mc-preview-head",
7591
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7592
+ className: "mc-preview-title",
7593
+ children: preview.relPath
7594
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7595
+ type: "button",
7596
+ className: "mc-preview-close",
7597
+ onClick: () => setPreview(null),
7598
+ children: t("previewClose")
7599
+ })]
7600
+ }), preview.content !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
7601
+ className: "mc-preview-body",
7602
+ children: preview.content
7603
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7604
+ className: "mc-preview-hint",
7605
+ children: preview.error ?? "…"
7606
+ })]
7607
+ })
6361
7608
  })
6362
- })] });
7609
+ ] });
6363
7610
  }
6364
7611
  //#endregion
6365
7612
  //#region src/client/index.ts
@@ -6374,21 +7621,18 @@ const inject = [
6374
7621
  "locale",
6375
7622
  "remote"
6376
7623
  ];
6377
- /** Register the 能力管理 section once `settings.section` is on the ledger. */
7624
+ /** Register the 能力菜单 section once `settings.section` is on the ledger. */
6378
7625
  async function apply(ctx) {
6379
7626
  const zh = {
6380
- nav: "能力管理",
6381
- title: "能力管理",
7627
+ nav: "能力菜单",
7628
+ title: "能力菜单",
6382
7629
  desc: "管理工具与技能的 常驻 / 按需 / 禁用 三档分类。",
6383
7630
  resident: "Resident(常驻上下文)",
6384
7631
  "on-demand": "On-demand(按需发现)",
6385
7632
  disabled: "Disabled(禁用)",
6386
- kind: "类型",
6387
- class: "分类",
6388
7633
  tool: "tool",
6389
7634
  skill: "skill",
6390
7635
  mandatory: "meta",
6391
- rules: "规则",
6392
7636
  toolsGroup: "Tools",
6393
7637
  skillsGroup: "Skills",
6394
7638
  builtInGroup: "系统内置",
@@ -6407,10 +7651,69 @@ async function apply(ctx) {
6407
7651
  previewClose: "关闭",
6408
7652
  detailNotFound: "未找到该工具的详情",
6409
7653
  cycleOverridden: "分类未生效:{count} 个能力被更高优先级规则覆盖(如通配规则),可移除对应通配规则后重试",
6410
- viewCatalog: "查看能力目录",
7654
+ refresh: "刷新",
7655
+ refreshing: "刷新中…",
7656
+ refreshFailed: "刷新失败,请查看日志",
7657
+ retry: "重试",
7658
+ carrierFailureHint: "请求未到达服务端:浏览器与 dsh 之间的连接已断开(Failed to fetch 属传输层失败,服务端方法未执行)。若刚重启过 dsh web,请先硬刷新页面(Ctrl/Cmd+Shift+R)再重试。",
7659
+ registerCapability: "注册能力",
7660
+ editMcp: "编辑 MCP 服务器",
7661
+ editSkillNamed: "编辑 Skill 目录:{name}",
7662
+ edit: "编辑",
7663
+ save: "保存",
7664
+ remove: "移除",
7665
+ cancel: "取消",
7666
+ confirmRemove: "确认移除",
7667
+ confirmRemoveMcp: "移除 MCP 服务器「{name}」?其 patch 文件中的配置行(含 headers、环境变量)将被删除,mcp__{name}__* 工具随之消失。",
7668
+ confirmRemoveSkillLink: "移除 Skill「{name}」?仅删除 ~/.dsh/skills 下的软链,源目录不受影响。",
7669
+ confirmRemoveSkillDir: "移除 Skill「{name}」?该项位于 ~/.dsh/skills 下且非软链,将连同文件递归删除,且不可恢复。",
7670
+ confirmSaveAnyway: "仍然保存",
7671
+ saveConfirmRealDir: "「{name}」这个条目是真实目录、不是软链:保存会先把它连同文件递归删除,再改为指向新路径,且不可恢复。",
7672
+ register: "注册",
7673
+ notEditable: "该项未在注册表中(可能是项目技能或内置技能),无法编辑。",
7674
+ entryNotFound: "目标已不存在,可能已被移除。",
7675
+ mcpServers: "MCP 服务器",
7676
+ skillDirs: "Skill 目录",
7677
+ skillDirPath: "技能来源路径",
7678
+ skillRoot: "技能位置",
7679
+ skillRootUser: "全局技能(~/.dsh/skills)",
7680
+ skillRootProject: "项目技能(<项目根>/.dsh/skills)",
7681
+ skillRootHintUser: "所有会话都能看到该技能。",
7682
+ skillRootHintProject: "只对 cwd 落在该项目内的会话可见。",
7683
+ skillProjectPath: "目标项目路径",
7684
+ skillProjectPathHint: "项目内任意一个已存在的路径即可;项目根按 dsh 的规则确定(向上找最近的 .git),技能写进 <项目根>/.dsh/skills。",
7685
+ skillRegisteredAt: "已注册到 {path}",
7686
+ skillRepointed: "已改为指向 {dir}",
7687
+ skillAdopted: "已纳入管理:{path}",
7688
+ skillSource: "来源:{source}",
7689
+ skillSourceHint: "该技能由 dsh 从其它根发现,不在本插件管理的技能目录里,因此没有「编辑」。",
7690
+ skillUnmanaged: "未纳入管理",
7691
+ sourceCustom: "自定义技能目录",
7692
+ sourceBundled: "随 dsh 预置",
7693
+ adoptSkill: "纳入管理",
7694
+ adoptSkillHint: "把它软链进 dsh 的技能根目录(默认 ~/.dsh/skills),内容不动;之后就能编辑与移除了。",
7695
+ adoptSkillTitle: "纳入管理「{name}」?",
7696
+ skillDirHint: "目录内需有 SKILL.md(按 dsh 的 skill 格式校验)。技能名以 SKILL.md 里声明的为准,目录名只决定注册后的软链名,两者不同也可以。",
7697
+ serverName: "serverName",
7698
+ serverNameImmutable: "serverName 不可修改:它构成工具名前缀 mcp__<serverName>__<tool>,并已被既有会话历史与权限规则引用;修改后这些记录将不再匹配。",
7699
+ transport: "传输方式",
7700
+ transportStdio: "stdio(本地子进程)",
7701
+ transportHttp: "streamable-http(HTTP MCP 端点)",
7702
+ transportHintStdio: "由客户端将该 MCP server 作为子进程启动,JSON-RPC 报文经标准输入 / 输出交换。",
7703
+ transportHintHttp: "该 MCP server 作为独立进程运行,可服务多个客户端连接;客户端经单个支持 POST / GET 的 HTTP 端点(MCP endpoint)与其通信。",
7704
+ command: "命令",
7705
+ args: "参数(空格分隔)",
7706
+ cwd: "工作目录",
7707
+ env: "环境变量",
7708
+ url: "URL",
7709
+ headers: "请求头",
7710
+ headersHint: "每行一个 Header,格式 Key: Value;Bearer token 等认证凭据在此填写。",
7711
+ timeout: "超时(秒)",
7712
+ timeoutInvalid: "超时须为数字,单位为秒。",
7713
+ viewCatalog: "策略与目录",
6411
7714
  catalogPolicy: "三档策略配置",
6412
7715
  catalogOnDemand: "按需能力目录",
6413
- catalogPolicyNote: "实时生成,只读;持久化入口为 cordis.patch.yml(未列出规则的能力默认常驻)",
7716
+ catalogPolicyNote: "实时生成;点选改动会在停手后自动写回 cordis.patch.yml(未列出规则的能力默认常驻)",
6414
7717
  catalogDisabled: "按需能力目录未启用(catalogFile 为空)",
6415
7718
  catalogUnreadable: "按需能力目录文件读取失败"
6416
7719
  };
@@ -6421,12 +7724,9 @@ async function apply(ctx) {
6421
7724
  resident: "Resident",
6422
7725
  "on-demand": "On-demand",
6423
7726
  disabled: "Disabled",
6424
- kind: "kind",
6425
- class: "class",
6426
7727
  tool: "tool",
6427
7728
  skill: "skill",
6428
7729
  mandatory: "meta",
6429
- rules: "rules",
6430
7730
  toolsGroup: "Tools",
6431
7731
  skillsGroup: "Skills",
6432
7732
  builtInGroup: "System built-in",
@@ -6445,10 +7745,69 @@ async function apply(ctx) {
6445
7745
  previewClose: "Close",
6446
7746
  detailNotFound: "Tool detail not found",
6447
7747
  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",
7748
+ refresh: "Refresh",
7749
+ refreshing: "Refreshing…",
7750
+ refreshFailed: "Refresh failed; see the log",
7751
+ retry: "Retry",
7752
+ carrierFailureHint: "The request never reached the server: the browser connection to dsh is broken (\"Failed to fetch\" is a transport failure, so no server method ran). If dsh web was just restarted, hard-refresh the page (Ctrl/Cmd+Shift+R) and retry.",
7753
+ registerCapability: "Register capability",
7754
+ editMcp: "Edit MCP server",
7755
+ editSkillNamed: "Edit skill directory: {name}",
7756
+ edit: "Edit",
7757
+ save: "Save",
7758
+ remove: "Remove",
7759
+ cancel: "Cancel",
7760
+ confirmRemove: "Confirm removal",
7761
+ 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.",
7762
+ confirmRemoveSkillLink: "Unregister the skill \"{name}\"? Only the symlink under ~/.dsh/skills is removed; the source directory is left untouched.",
7763
+ 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.",
7764
+ confirmSaveAnyway: "Save anyway",
7765
+ 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.",
7766
+ register: "Register",
7767
+ notEditable: "This entry is not in the registry (a project or bundled skill), so it cannot be edited.",
7768
+ entryNotFound: "The target no longer exists; it may have been removed.",
7769
+ mcpServers: "MCP servers",
7770
+ skillDirs: "Skill directories",
7771
+ skillDirPath: "Skill source path",
7772
+ skillRoot: "Skill location",
7773
+ skillRootUser: "Global (~/.dsh/skills)",
7774
+ skillRootProject: "Project (<projectRoot>/.dsh/skills)",
7775
+ skillRootHintUser: "Visible to every session.",
7776
+ skillRootHintProject: "Visible only to sessions whose cwd sits inside that project.",
7777
+ skillProjectPath: "Target project path",
7778
+ 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.",
7779
+ skillRegisteredAt: "Registered at {path}",
7780
+ skillRepointed: "Repointed to {dir}",
7781
+ skillAdopted: "Adopted: {path}",
7782
+ skillSource: "Source: {source}",
7783
+ skillSourceHint: "dsh found this skill in another root, not in a skill directory this plugin manages, so there is no Edit button.",
7784
+ skillUnmanaged: "Not managed here",
7785
+ sourceCustom: "custom skill dirs",
7786
+ sourceBundled: "bundled with dsh",
7787
+ adoptSkill: "Adopt",
7788
+ adoptSkillHint: "Link it into dsh's skill root (default ~/.dsh/skills); the content is untouched, and it becomes editable and removable.",
7789
+ adoptSkillTitle: "Adopt \"{name}\"?",
7790
+ skillDirHint: "The directory must hold a SKILL.md in dsh's skill format. The skill name is the one declared in SKILL.md; the directory name only names the link this registers, so the two may differ.",
7791
+ serverName: "serverName",
7792
+ serverNameImmutable: "serverName is immutable: it forms the tool name prefix mcp__<serverName>__<tool> and is referenced by existing session history and permission rules, which stop matching once it changes.",
7793
+ transport: "Transport",
7794
+ transportStdio: "stdio (local subprocess)",
7795
+ transportHttp: "streamable-http (HTTP MCP endpoint)",
7796
+ transportHintStdio: "The client launches the MCP server as a subprocess and exchanges JSON-RPC messages over its standard input and output.",
7797
+ transportHintHttp: "The server runs as an independent process that can handle multiple client connections; the client reaches it over a single HTTP endpoint (the MCP endpoint) supporting POST and GET.",
7798
+ command: "Command",
7799
+ args: "Args (space separated)",
7800
+ cwd: "Working directory",
7801
+ env: "Environment",
7802
+ url: "URL",
7803
+ headers: "Headers",
7804
+ headersHint: "One header per line as Key: Value; credentials such as a Bearer token go here.",
7805
+ timeout: "Timeout (seconds)",
7806
+ timeoutInvalid: "Timeout must be a number of seconds.",
7807
+ viewCatalog: "Policy & catalog",
6449
7808
  catalogPolicy: "Policy (effective)",
6450
7809
  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.",
7810
+ 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
7811
  catalogDisabled: "On-demand catalog emission is disabled (catalogFile is empty).",
6453
7812
  catalogUnreadable: "Failed to read the on-demand catalog file."
6454
7813
  };
@@ -6474,17 +7833,10 @@ async function apply(ctx) {
6474
7833
  }
6475
7834
  };
6476
7835
  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
7836
  return {
6484
- remote: namespace,
7837
+ remote: remote(),
6485
7838
  t,
6486
- ...mountError !== void 0 ? { mountError } : {},
6487
- ...remoteKeys !== void 0 ? { remoteKeys } : {}
7839
+ ...mountError !== void 0 ? { mountError } : {}
6488
7840
  };
6489
7841
  };
6490
7842
  ctx.slots.inject("settings.section", () => ctx.slots.register({