dsh-skill-hub 0.3.12 → 0.3.14

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/CONTRIBUTING.md CHANGED
@@ -39,7 +39,7 @@ src/client/locales/ dictionaries by view (common/skills/market/sources/detai
39
39
  ```bash
40
40
  npm install
41
41
  npm run typecheck # tsc --noEmit
42
- npm test # vitest (217 tests across 13 suites)
42
+ npm test # vitest (233 tests across 14 suites)
43
43
  npm run build # tsc declarations + tsdown bundles (lib/index.js + lib/client.js)
44
44
  ```
45
45
 
@@ -68,10 +68,12 @@ npm run build # tsc declarations + tsdown bundles (lib/index.js + lib/clie
68
68
  ```bash
69
69
  # after a change:
70
70
  npm run build
71
- cp lib/index.js lib/client.js ~/.dsh/profiles/web/node_modules/dsh-skill-hub/lib/
72
71
  # restart the dsh web process, then verify Settings → 技能 and Settings → 插件 → Skill Hub
73
72
  ```
74
73
 
74
+ When the web profile installs this repo as a link (`"dsh-skill-hub": "link:/path/to/repo"`, the
75
+ usual local-dev setup), `lib/` is picked up on the next `dsh web` restart — no copy step needed.
76
+
75
77
  ## Issues
76
78
 
77
79
  - **Bugs**: include the dsh version, Node version, the plugin version, and the exact steps.
package/README.md CHANGED
@@ -24,7 +24,7 @@ dsh plugin --profile web add dsh-skill-hub
24
24
  # restart dsh web → Settings → 技能 → Market → scan → import
25
25
  ```
26
26
 
27
- Requires `Node ^22.19 || >=24` + dsh web (`0.1.5-rc.2`, `0.1.x` forward compatible).
27
+ Requires `Node ^22.19 || >=24` + dsh web `>=0.1.6-alpha.2 <0.2`.
28
28
 
29
29
  ## Features
30
30
 
@@ -115,6 +115,7 @@ npm run build # tsc + tsdown → lib/index.js + lib/client.js
115
115
 
116
116
  - `duplicate loader entry id: skill-hub` — remove the duplicate install (keep one `dsh plugin add` method).
117
117
  - Skill missing — check the diagnostics section (frontmatter / name mismatch / short description).
118
+ - Empty source group — its skills were deleted, or their disabled records were lost (sidecar restored/hand-edited). Startup reconciles `.disabled` files on disk; source groups with no visible member are no longer rendered.
118
119
  - Dots missing in `/` menu — dsh internals changed; catalog still works.
119
120
 
120
121
  ## Community
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@ dsh plugin --profile web add dsh-skill-hub
24
24
  # 重启 dsh web → 设置 → 技能 → 市场 → 扫描 → 导入
25
25
  ```
26
26
 
27
- 要求 `Node ^22.19 || >=24` + dsh web(`0.1.5-rc.2`,兼容后续 `0.1.x`)。
27
+ 要求 `Node ^22.19 || >=24` + dsh web `>=0.1.6-alpha.2 <0.2`。
28
28
 
29
29
  ## 功能
30
30
 
@@ -115,6 +115,7 @@ npm run build # tsc + tsdown → lib/index.js + lib/client.js
115
115
 
116
116
  - `duplicate loader entry id: skill-hub` — 删掉重复安装(只留一种 `dsh plugin add`)。
117
117
  - 技能不出现 — 看诊断区(缺 frontmatter / 名称不一致 / 描述过短)。
118
+ - 来源组空壳 — 组内技能已被删除、或禁用记录丢失(状态文件被恢复/手改)。启动时自动对账磁盘上的 `.disabled` 文件补记录;无可见成员的来源组不再渲染。
118
119
  - `/` 菜单圆点消失 — dsh 内部触发源变更,目录功能不受影响。
119
120
 
120
121
  ## 社区
package/lib/client.js CHANGED
@@ -959,6 +959,27 @@ window.__ModuleLoader__.load({
959
959
  });
960
960
  }
961
961
  /**
962
+ * Match origin collections against the currently visible enabled skills and
963
+ * disabled records, dropping collections with no visible member. Without
964
+ * this, a stale origin (skill deleted on disk, or a `.disabled` file whose
965
+ * sidecar record was lost) renders a group header with zero rows — an empty
966
+ * shell the sources tab otherwise never shows (project/personal groups
967
+ * already disappear when they have nothing to display).
968
+ */
969
+ function visibleCollections(collections, visibleSkills, disabledRecords, normalized, sourceFilter, origins) {
970
+ const visible = [];
971
+ for (const collection of collections) {
972
+ const skills = visibleSkills.filter((skill) => collection.skillNames.includes(skill.name));
973
+ const disabledMembers = disabledRecords.filter((record) => collection.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter));
974
+ if (skills.length > 0 || disabledMembers.length > 0) visible.push({
975
+ collection,
976
+ skills,
977
+ disabledMembers
978
+ });
979
+ }
980
+ return visible;
981
+ }
982
+ /**
962
983
  * Sort a skill list in place-safe copy order: name ascending, added
963
984
  * descending (newest first, unknown addedAt last), or uses descending
964
985
  * (most-called first). Unknown values always trail.
@@ -1143,36 +1164,11 @@ window.__ModuleLoader__.load({
1143
1164
  }
1144
1165
  }
1145
1166
  /**
1146
- * One menu-row dot element. Inline span so it needs no CSS module; the
1147
- * candidate menu centers it inside its 16×16 leading icon slot.
1148
- * @param color - the dot's background color.
1149
- * @returns a React node (memory-only, never crosses the Host boundary).
1150
- */
1151
- function dotIcon(color) {
1152
- return (0, react.createElement)("span", {
1153
- "aria-hidden": true,
1154
- style: {
1155
- display: "inline-block",
1156
- width: 6,
1157
- height: 6,
1158
- borderRadius: 3,
1159
- background: color,
1160
- flex: "none"
1161
- }
1162
- });
1163
- }
1164
- /**
1165
- * Wrap the core skill source so every menu row carries the invocation dot.
1166
- * Exported for unit tests; production wiring goes through setupSkillSlashDots.
1167
- * @param source - the registered `/skill` source.
1168
- * @param api - hub browser API for the modelInvocable lookup.
1169
- * @param scope - hub settings scope for the dot colors.
1170
- * @returns a disposer restoring the original candidates.
1171
- */
1172
- /**
1173
- * DOM 兜底:在 alpha.2 新版 MenuView(icon 仅枚举)下通过直接操作
1174
- * 已渲染的 `[role="option"]` 列表注入彩色点,绕过 `icon` 限制。
1175
- * 旧版仍走 `icon` 注入,此处仅为新版。
1167
+ * 通过直接操作已渲染的 `[role="option"]` 列表注入彩色点,
1168
+ * 绕过 MenuView `icon` 的枚举限制。
1169
+ * @param modelByName - 技能名 模型可调用性;不在表内的候选不注点。
1170
+ * @param modelColor - 模型可调用技能的点色。
1171
+ * @param userColor - 仅用户可调用技能的点色。
1176
1172
  */
1177
1173
  function injectDotsViaDOM(modelByName, modelColor, userColor) {
1178
1174
  if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return;
@@ -1210,25 +1206,24 @@ window.__ModuleLoader__.load({
1210
1206
  };
1211
1207
  requestAnimationFrame(() => setTimeout(run, 0));
1212
1208
  }
1209
+ /**
1210
+ * Wrap the core skill source so every menu row carries the invocation dot.
1211
+ * Exported for unit tests; production wiring goes through setupSkillSlashDots.
1212
+ * @param source - the registered `/skill` source.
1213
+ * @param api - hub browser API for the modelInvocable lookup.
1214
+ * @param scope - hub settings scope for the dot colors.
1215
+ * @returns a disposer restoring the original candidates.
1216
+ */
1213
1217
  function wrapSkillSource(source, api, scope) {
1214
1218
  const original = source.candidates;
1215
1219
  source.candidates = async (session, req) => {
1216
1220
  const items = await original(session, req);
1217
1221
  if (req.signal.aborted) return items;
1218
- const isNewHost = req !== null && typeof req === "object" && "drilled" in req;
1219
1222
  const modelByName = await modelInvocableMap(api);
1220
1223
  if (req.signal.aborted) return items;
1221
1224
  const snapshot = scope.getSnapshot();
1222
- const modelColor = snapshot.value?.dotModelColor ?? "#2f81f7";
1223
- const userColor = snapshot.value?.dotUserColor ?? "#3fb950";
1224
- if (isNewHost) {
1225
- injectDotsViaDOM(modelByName, modelColor, userColor);
1226
- return items;
1227
- }
1228
- return items.map((item) => ({
1229
- ...item,
1230
- icon: dotIcon(modelByName.get(item.name) ?? true ? modelColor : userColor)
1231
- }));
1225
+ injectDotsViaDOM(modelByName, snapshot.value?.dotModelColor ?? "#2f81f7", snapshot.value?.dotUserColor ?? "#3fb950");
1226
+ return items;
1232
1227
  };
1233
1228
  return () => {
1234
1229
  source.candidates = original;
@@ -2564,8 +2559,10 @@ window.__ModuleLoader__.load({
2564
2559
  * The dsh-skill-hub plugin settings card: bridges the hub's settings
2565
2560
  * namespace (bound through the official settings transport) onto the
2566
2561
  * family-style staged card form (enabled master switch + agent announcement).
2567
- * Registered into the official `settings.plugin.item` slot keyed by that
2568
- * namespace, so the plugin shows up in Settings 插件 on dsh rc.7+.
2562
+ * Registered into `plugins.bundle.config` under the bundle's package name, so
2563
+ * the Plugins manager renders it on dsh-skill-hub's own page. That slot is
2564
+ * dispatched with `view: 'page'` only, so the card always renders its full
2565
+ * form (no `summary` one-liner variant).
2569
2566
  */
2570
2567
  /** Bridges the hub's config scope onto the card's staged form. */
2571
2568
  var SkillHubSettingsCardController = class {
@@ -3833,10 +3830,12 @@ window.__ModuleLoader__.load({
3833
3830
  const [topOverKey, setTopOverKey] = (0, react.useState)(null);
3834
3831
  /** 重复技能名集合:整表只建一次,行内用 has 取代逐行线性 includes。 */
3835
3832
  const duplicateNames = (0, react.useMemo)(() => new Set(catalog?.duplicateNames ?? []), [catalog]);
3836
- const projectSkillsAll = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
3833
+ const sourceFiltered = filterBySource(sorted, sourceFilter, origins);
3834
+ const projectSkillsAll = sourceFiltered.filter((skill) => isProjectSource(skill.source));
3837
3835
  const hasProject = projectSkillsAll.length > 0;
3838
- const collections = groupsState?.collections ?? [];
3839
- const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3836
+ const visible = visibleCollections(groupsState?.collections ?? [], sourceFiltered, catalog?.disabled ?? [], normalized, sourceFilter, origins);
3837
+ const collections = visible.map((entry) => entry.collection);
3838
+ const uncategorized = sourceFiltered.filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3840
3839
  const personalDisabled = (catalog?.disabled ?? []).filter((record) => origins[record.name] === void 0).filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)).filter((record) => sourceFilter === "all" || sourceFilter === "private");
3841
3840
  const allPersonalNames = [...uncategorized.map((s) => s.name), ...personalDisabled.map((r) => r.name)];
3842
3841
  const hasPersonal = allPersonalNames.length > 0;
@@ -3895,7 +3894,7 @@ window.__ModuleLoader__.load({
3895
3894
  openDetail: hub.openDetail,
3896
3895
  requestDeleteSkill: hub.requestDeleteSkill
3897
3896
  };
3898
- if (skillView === "flat") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: filterBySource(sorted, sourceFilter, origins).map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
3897
+ if (skillView === "flat") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: sourceFiltered.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
3899
3898
  skill,
3900
3899
  ...rowProps
3901
3900
  }, skill.name)) });
@@ -3933,10 +3932,9 @@ window.__ModuleLoader__.load({
3933
3932
  }, "project");
3934
3933
  if (topKey.startsWith("col:")) {
3935
3934
  const colName = topKey.slice(4);
3936
- const collection = collections.find((c) => c.name === colName);
3937
- if (collection === void 0) return null;
3938
- const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name));
3939
- const disabledMembers = (catalog?.disabled ?? []).filter((record) => collection.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter));
3935
+ const entry = visible.find((item) => item.collection.name === colName);
3936
+ if (entry === void 0) return null;
3937
+ const { collection, skills, disabledMembers } = entry;
3940
3938
  const collapsed = collapsedGroups.has("col:" + collection.name);
3941
3939
  const view = groupSwitchView(collection.skillNames, viewNames);
3942
3940
  const check = sourceCheck[collection.name];
@@ -4407,6 +4405,23 @@ window.__ModuleLoader__.load({
4407
4405
  function VersionChoiceDialog(props) {
4408
4406
  const { choice, busy, onSelect, onCustom, onCancel, onConfirm } = props;
4409
4407
  const effective = choice.custom.trim() !== "" ? choice.custom.trim() : choice.selected;
4408
+ const branchOptions = choice.branches.filter((branch) => !choice.releases.includes(branch));
4409
+ const [refGroup, setRefGroup] = (0, react.useState)(choice.releases.includes(choice.selected) || choice.branches.length === 0 ? "releases" : "branches");
4410
+ (0, react.useEffect)(() => {
4411
+ if (choice.loading) return;
4412
+ if (choice.releases.includes(choice.selected)) setRefGroup("releases");
4413
+ else if (branchOptions.includes(choice.selected)) setRefGroup("branches");
4414
+ }, [choice.loading, choice.repo]);
4415
+ const activeRefs = refGroup === "releases" ? choice.releases : branchOptions;
4416
+ const listedRefs = [...choice.releases, ...branchOptions];
4417
+ const currentUnlistedRef = choice.selected !== "" && !listedRefs.includes(choice.selected) ? choice.selected : void 0;
4418
+ const selectRefs = currentUnlistedRef !== void 0 ? [currentUnlistedRef, ...activeRefs] : activeRefs;
4419
+ const selectValue = selectRefs.includes(choice.selected) ? choice.selected : selectRefs[0] ?? "";
4420
+ const selectGroup = (next) => {
4421
+ setRefGroup(next);
4422
+ const nextRefs = next === "releases" ? choice.releases : branchOptions;
4423
+ if (nextRefs.length > 0 && !nextRefs.includes(choice.selected)) onSelect(nextRefs[0]);
4424
+ };
4410
4425
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
4411
4426
  onClose: onCancel,
4412
4427
  children: [
@@ -4422,38 +4437,42 @@ window.__ModuleLoader__.load({
4422
4437
  className: panel_module_css_default.dialogText,
4423
4438
  children: tt("market.scanning")
4424
4439
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4425
- choice.releases.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4426
- className: panel_module_css_default.dialogText,
4427
- style: { marginBottom: 4 },
4428
- children: tt("market.versionReleases")
4429
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4430
- className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4431
- value: choice.releases.includes(choice.selected) ? choice.selected : choice.releases[0],
4432
- onChange: (event) => {
4433
- onSelect(event.target.value);
4434
- onCustom("");
4435
- },
4436
- children: choice.releases.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4437
- value: tag,
4438
- children: tag
4439
- }, tag))
4440
- })] }) : null,
4441
- choice.branches.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4442
- className: panel_module_css_default.dialogText,
4443
- style: { marginBottom: 4 },
4444
- children: tt("market.versionBranches")
4445
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4441
+ className: panel_module_css_default.segmented,
4442
+ role: "group",
4443
+ "aria-label": tt("market.versionTitle"),
4444
+ style: { marginBottom: 8 },
4445
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4446
+ type: "button",
4447
+ className: panel_module_css_default.segBtn + (refGroup === "releases" ? " " + panel_module_css_default.segBtnActive : ""),
4448
+ disabled: choice.releases.length === 0,
4449
+ "aria-pressed": refGroup === "releases",
4450
+ onClick: () => {
4451
+ selectGroup("releases");
4452
+ },
4453
+ children: tt("market.versionReleases")
4454
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4455
+ type: "button",
4456
+ className: panel_module_css_default.segBtn + (refGroup === "branches" ? " " + panel_module_css_default.segBtnActive : ""),
4457
+ disabled: branchOptions.length === 0,
4458
+ "aria-pressed": refGroup === "branches",
4459
+ onClick: () => {
4460
+ selectGroup("branches");
4461
+ },
4462
+ children: tt("market.versionBranches")
4463
+ })]
4464
+ }),
4465
+ selectValue !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4446
4466
  className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4447
- value: choice.branches.includes(choice.selected) ? choice.selected : choice.branches[0],
4467
+ value: selectValue,
4448
4468
  onChange: (event) => {
4449
4469
  onSelect(event.target.value);
4450
- onCustom("");
4451
4470
  },
4452
- children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4453
- value: branch,
4454
- children: branch
4455
- }, branch))
4456
- })] }) : null,
4471
+ children: selectRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4472
+ value: ref,
4473
+ children: ref
4474
+ }, ref))
4475
+ }) : null,
4457
4476
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4458
4477
  className: panel_module_css_default.dialogText,
4459
4478
  style: { marginBottom: 4 },
@@ -5322,14 +5341,15 @@ window.__ModuleLoader__.load({
5322
5341
  choice: versionDialog,
5323
5342
  busy: versionBusy,
5324
5343
  onSelect: (selected) => {
5325
- setVersionDialog({
5326
- ...versionDialog,
5327
- selected
5344
+ setVersionDialog((previous) => previous === null ? previous : {
5345
+ ...previous,
5346
+ selected,
5347
+ custom: ""
5328
5348
  });
5329
5349
  },
5330
5350
  onCustom: (custom) => {
5331
- setVersionDialog({
5332
- ...versionDialog,
5351
+ setVersionDialog((previous) => previous === null ? previous : {
5352
+ ...previous,
5333
5353
  custom
5334
5354
  });
5335
5355
  },
@@ -7570,12 +7590,12 @@ window.__ModuleLoader__.load({
7570
7590
  const scope = ctx.settingsScope.bind({ namespace: NS });
7571
7591
  const settingsCard = new SkillHubSettingsCardController(scope);
7572
7592
  ctx.effect(() => setupSkillSlashDots(ctx, api, scope), "dsh-skill-hub: slash dots");
7573
- ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
7574
- name: "settings.plugin.item",
7575
- key: NS,
7593
+ ctx.effect(() => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
7594
+ name: "plugins.bundle.config",
7595
+ key: "dsh-skill-hub",
7576
7596
  locale: NS,
7577
7597
  inject: () => settingsCard.inject()
7578
- }, SkillHubSettingsCard)), "dsh-skill-hub: settings card");
7598
+ }, SkillHubSettingsCard)), "dsh-skill-hub: plugin config");
7579
7599
  ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
7580
7600
  name: "settings.section",
7581
7601
  id: "skill-hub",