dsh-skill-hub 0.2.5 → 0.3.0

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
@@ -32,11 +32,16 @@ window.__ModuleLoader__.load({
32
32
  marketSync: "/api/skill-hub/market/source/sync",
33
33
  repo: "/api/skill-hub/repo",
34
34
  repoImport: "/api/skill-hub/repo/import",
35
+ repoImportProgress: "/api/skill-hub/repo/import/progress",
36
+ repoImportCancel: "/api/skill-hub/repo/import/cancel",
35
37
  update: "/api/skill-hub/update",
36
38
  groups: "/api/skill-hub/groups",
37
39
  tag: "/api/skill-hub/tag",
38
40
  tagDelete: "/api/skill-hub/tag/delete",
39
41
  tagMembers: "/api/skill-hub/tag/members",
42
+ tagReorder: "/api/skill-hub/tag/reorder",
43
+ collectionReorder: "/api/skill-hub/collections/reorder",
44
+ sourceGroupReorder: "/api/skill-hub/source-groups/reorder",
40
45
  sources: "/api/skill-hub/sources",
41
46
  sourceCheck: "/api/skill-hub/sources/check",
42
47
  sourceSync: "/api/skill-hub/sources/sync",
@@ -188,7 +193,7 @@ window.__ModuleLoader__.load({
188
193
  async repoDiscover(repo) {
189
194
  return readJson(await fetchWithTimeout(SKILL_HUB_API.repo + "?repo=" + encodeURIComponent(repo)));
190
195
  }
191
- /** Install selected repo skills into the user-dsh root (records the source). */
196
+ /** Install selected repo skills into the user-dsh root (records the source). Returns jobId for polling. */
192
197
  async repoImport(repo, paths, ref) {
193
198
  const payload = {
194
199
  repo,
@@ -201,6 +206,18 @@ window.__ModuleLoader__.load({
201
206
  body: JSON.stringify(payload)
202
207
  }));
203
208
  }
209
+ /** Poll import job progress (B方案轮询) */
210
+ async repoImportProgress(jobId) {
211
+ return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImportProgress + "?jobId=" + encodeURIComponent(jobId)));
212
+ }
213
+ /** Cancel a running import job (选项2:唯有取消才停) */
214
+ async repoImportCancel(jobId) {
215
+ return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImportCancel, {
216
+ method: "POST",
217
+ headers: { "content-type": "application/json" },
218
+ body: JSON.stringify({ jobId })
219
+ }));
220
+ }
204
221
  /** Read the hub's runtime config (effective values + saved overrides). */
205
222
  async config() {
206
223
  return readJson(await fetchWithTimeout(SKILL_HUB_API.config));
@@ -244,6 +261,30 @@ window.__ModuleLoader__.load({
244
261
  })
245
262
  }))).tags;
246
263
  }
264
+ /** 拖拽重排场景分组 */
265
+ async reorderTags(orderedIds) {
266
+ return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagReorder, {
267
+ method: "POST",
268
+ headers: { "content-type": "application/json" },
269
+ body: JSON.stringify({ orderedIds })
270
+ }))).tags;
271
+ }
272
+ /** 拖拽重排来源集合 */
273
+ async reorderCollections(orderedNames) {
274
+ return (await readJson(await fetchWithTimeout(SKILL_HUB_API.collectionReorder, {
275
+ method: "POST",
276
+ headers: { "content-type": "application/json" },
277
+ body: JSON.stringify({ orderedNames })
278
+ }))).collections;
279
+ }
280
+ /** 拖拽重排来源顶层分组(project / collections / personal) */
281
+ async reorderSourceGroups(orderedKeys) {
282
+ return (await readJson(await fetchWithTimeout(SKILL_HUB_API.sourceGroupReorder, {
283
+ method: "POST",
284
+ headers: { "content-type": "application/json" },
285
+ body: JSON.stringify({ orderedKeys })
286
+ }))).order;
287
+ }
247
288
  /** 来源列表 + 派生 origin 映射 + 集合组 + 回收站。 */
248
289
  async sources() {
249
290
  return readJson(await fetchWithTimeout(SKILL_HUB_API.sources));
@@ -351,6 +392,11 @@ window.__ModuleLoader__.load({
351
392
  "market.scan": "扫描",
352
393
  "market.scanning": "扫描中…",
353
394
  "market.check": "检查",
395
+ "market.checkUpdate": "检查更新",
396
+ "market.checking": "检查中…",
397
+ "market.upToDate": "已是最新",
398
+ "market.update": "更新",
399
+ "market.updateCount": "更新 {count} 个",
354
400
  "market.sync": "同步",
355
401
  "market.syncing": "同步中…",
356
402
  "market.updated": "有更新",
@@ -374,8 +420,12 @@ window.__ModuleLoader__.load({
374
420
  "repo.imported": "导入成功 {count} 个",
375
421
  "repo.skippedExisting": "跳过已存在 {count} 个",
376
422
  "repo.failed": "失败 {count} 个",
423
+ "repo.cancel": "取消",
424
+ "repo.cancelled": "已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理",
425
+ "repo.importingCurrent": "正在导入: {name} ({done}/{total})",
377
426
  "repo.root.skills": "技能 skills",
378
427
  "repo.root.designTemplates": "模板 design-templates",
428
+ "repo.root.generic": "{root}",
379
429
  "update.check": "检查更新",
380
430
  "update.checking": "检查中…",
381
431
  "update.upToDate": "已是最新版本 v{version}",
@@ -462,6 +512,10 @@ window.__ModuleLoader__.load({
462
512
  "source.followDelete": "跟进删除",
463
513
  "source.deleteConfirmTitle": "跟进上游删除?",
464
514
  "source.deleteConfirmText": "以下技能已从上游仓库移除,本地目录将移入回收站(可恢复):",
515
+ "source.deleteGroup": "删除整组",
516
+ "source.deleteGroupHint": "删除该分组下的全部 {count} 个技能",
517
+ "source.deleteGroupTitle": "删除整组?",
518
+ "source.deleteGroupText": "将把分组“{name}”下的全部 {count} 个技能移入回收站(可恢复),只读技能会跳过:",
465
519
  "source.trash": "回收站",
466
520
  "source.restore": "恢复",
467
521
  "source.clearTrash": "清空回收站",
@@ -479,7 +533,7 @@ window.__ModuleLoader__.load({
479
533
  "form.success": "已创建:",
480
534
  "form.error": "创建失败:",
481
535
  "err.dismiss": "知道了",
482
- "settings.title": "Skill Hub",
536
+ "settings.title": "技能中枢",
483
537
  "settings.description": "管理 dsh 本地技能目录:浏览、分组、来源同步与新建。",
484
538
  "settings.enabled": "启用插件",
485
539
  "settings.enabledHint": "关闭后技能中枢的路由、入口与公告全部下线。",
@@ -563,6 +617,11 @@ window.__ModuleLoader__.load({
563
617
  "market.scan": "Scan",
564
618
  "market.scanning": "Scanning…",
565
619
  "market.check": "Check",
620
+ "market.checkUpdate": "Check for updates",
621
+ "market.checking": "Checking…",
622
+ "market.upToDate": "Up to date",
623
+ "market.update": "Update",
624
+ "market.updateCount": "Update {count}",
566
625
  "market.sync": "Sync",
567
626
  "market.syncing": "Syncing…",
568
627
  "market.updated": "Updates available",
@@ -586,8 +645,12 @@ window.__ModuleLoader__.load({
586
645
  "repo.imported": "Imported {count}",
587
646
  "repo.skippedExisting": "Skipped existing {count}",
588
647
  "repo.failed": "Failed {count}",
648
+ "repo.cancel": "Cancel",
649
+ "repo.cancelled": "Cancelled · {imported}/{total} kept, temp files cleaned",
650
+ "repo.importingCurrent": "Importing: {name} ({done}/{total})",
589
651
  "repo.root.skills": "Skills",
590
652
  "repo.root.designTemplates": "Templates design-templates",
653
+ "repo.root.generic": "{root}",
591
654
  "update.check": "Check for updates",
592
655
  "update.checking": "Checking…",
593
656
  "update.upToDate": "Up to date: v{version}",
@@ -674,6 +737,10 @@ window.__ModuleLoader__.load({
674
737
  "source.followDelete": "Follow deletion",
675
738
  "source.deleteConfirmTitle": "Follow upstream deletion?",
676
739
  "source.deleteConfirmText": "These skills were removed upstream; their local directories move to trash (restorable):",
740
+ "source.deleteGroup": "Delete group",
741
+ "source.deleteGroupHint": "Delete all {count} skills in this group",
742
+ "source.deleteGroupTitle": "Delete group?",
743
+ "source.deleteGroupText": "All {count} skills in group \"{name}\" will be moved to trash (restorable); read-only skills will be skipped:",
677
744
  "source.trash": "Trash",
678
745
  "source.restore": "Restore",
679
746
  "source.clearTrash": "Empty trash",
@@ -1112,7 +1179,7 @@ window.__ModuleLoader__.load({
1112
1179
  }
1113
1180
  //#endregion
1114
1181
  //#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/settings-card.module.css.mjs
1115
- const css$1 = ".TqD_hG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.TqD_hG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.TqD_hG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.TqD_hG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.TqD_hG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.TqD_hG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.TqD_hG_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_chevron,.TqD_hG_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.TqD_hG_chevronOpen{transform:rotate(180deg)}.TqD_hG_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.TqD_hG_readOnly,.TqD_hG_notExposed{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.TqD_hG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.TqD_hG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.TqD_hG_discard,.TqD_hG_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.TqD_hG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.TqD_hG_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.TqD_hG_discard:disabled,.TqD_hG_save:disabled{opacity:.4;cursor:default}.TqD_hG_discard:focus-visible,.TqD_hG_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.TqD_hG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.TqD_hG_field+.TqD_hG_field{border-top:1px solid var(--dsw-alias-border-l2)}.TqD_hG_head{align-items:center;gap:8px;display:flex}.TqD_hG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.TqD_hG_badges{align-items:center;gap:8px;display:inline-flex}.TqD_hG_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.TqD_hG_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.TqD_hG_reset:disabled{cursor:default}.TqD_hG_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.TqD_hG_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.TqD_hG_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.TqD_hG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.TqD_hG_colorRow{align-items:center;gap:8px;display:flex}.TqD_hG_colorInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);cursor:pointer;border-radius:8px;width:34px;height:34px;padding:0}.TqD_hG_colorInput:disabled{opacity:.5;cursor:default}.TqD_hG_colorInput::-webkit-color-swatch-wrapper{padding:2px}.TqD_hG_colorInput::-webkit-color-swatch{border:none;border-radius:4px}.TqD_hG_colorText{flex:1;min-width:0}.TqD_hG_switchRow{align-items:center;gap:12px;display:flex}.TqD_hG_switchText{flex:1;min-width:0}.TqD_hG_switch,.TqD_hG_switchOn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);cursor:pointer;border-radius:999px;flex:none;width:38px;height:22px;padding:2px;transition:background .15s,border-color .15s}.TqD_hG_switchOn{border-color:var(--dsw-alias-state-business-primary,#2f81f7);background:var(--dsw-alias-state-business-primary,#2f81f7)}.TqD_hG_switch:disabled,.TqD_hG_switchOn:disabled{opacity:.4;cursor:default}.TqD_hG_switchThumb{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}.TqD_hG_switchOn .TqD_hG_switchThumb{transform:translate(16px)}";
1182
+ const css$1 = ".TqD_hG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.TqD_hG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.TqD_hG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.TqD_hG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.TqD_hG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.TqD_hG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.TqD_hG_versionTag{opacity:.45;white-space:nowrap;font-variant-numeric:tabular-nums;vertical-align:1px;margin-left:6px;font-size:12px;font-weight:500}.TqD_hG_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_chevron,.TqD_hG_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.TqD_hG_chevronOpen{transform:rotate(180deg)}.TqD_hG_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.TqD_hG_readOnly,.TqD_hG_notExposed{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.TqD_hG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.TqD_hG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.TqD_hG_discard,.TqD_hG_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.TqD_hG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.TqD_hG_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.TqD_hG_discard:disabled,.TqD_hG_save:disabled{opacity:.4;cursor:default}.TqD_hG_discard:focus-visible,.TqD_hG_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.TqD_hG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.TqD_hG_field+.TqD_hG_field{border-top:1px solid var(--dsw-alias-border-l2)}.TqD_hG_head{align-items:center;gap:8px;display:flex}.TqD_hG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.TqD_hG_badges{align-items:center;gap:8px;display:inline-flex}.TqD_hG_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.TqD_hG_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.TqD_hG_reset:disabled{cursor:default}.TqD_hG_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.TqD_hG_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.TqD_hG_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.TqD_hG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.TqD_hG_colorRow{align-items:center;gap:8px;display:flex}.TqD_hG_colorInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);cursor:pointer;border-radius:8px;width:34px;height:34px;padding:0}.TqD_hG_colorInput:disabled{opacity:.5;cursor:default}.TqD_hG_colorInput::-webkit-color-swatch-wrapper{padding:2px}.TqD_hG_colorInput::-webkit-color-swatch{border:none;border-radius:4px}.TqD_hG_colorText{flex:1;min-width:0}.TqD_hG_switchRow{align-items:center;gap:12px;display:flex}.TqD_hG_switchText{flex:1;min-width:0}.TqD_hG_switch,.TqD_hG_switchOn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);cursor:pointer;border-radius:999px;flex:none;width:38px;height:22px;padding:2px;transition:background .15s,border-color .15s}.TqD_hG_switchOn{border-color:var(--dsw-alias-state-business-primary,#2f81f7);background:var(--dsw-alias-state-business-primary,#2f81f7)}.TqD_hG_switch:disabled,.TqD_hG_switchOn:disabled{opacity:.4;cursor:default}.TqD_hG_switchThumb{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}.TqD_hG_switchOn .TqD_hG_switchThumb{transform:translate(16px)}";
1116
1183
  const tagId$1 = "dsh-skill-hub/settings-card.module.css";
1117
1184
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1118
1185
  const tag = document.createElement("style");
@@ -1153,7 +1220,8 @@ window.__ModuleLoader__.load({
1153
1220
  "switchOn": "TqD_hG_switchOn",
1154
1221
  "switchRow": "TqD_hG_switchRow",
1155
1222
  "switchText": "TqD_hG_switchText",
1156
- "switchThumb": "TqD_hG_switchThumb"
1223
+ "switchThumb": "TqD_hG_switchThumb",
1224
+ "versionTag": "TqD_hG_versionTag"
1157
1225
  };
1158
1226
  //#endregion
1159
1227
  //#region src/client/settings-card.tsx
@@ -1186,9 +1254,12 @@ window.__ModuleLoader__.load({
1186
1254
  children: [
1187
1255
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1188
1256
  className: settings_card_module_css_default.headText,
1189
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1257
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1190
1258
  className: settings_card_module_css_default.name,
1191
- children: title
1259
+ children: [title, props.version !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1260
+ className: settings_card_module_css_default.versionTag,
1261
+ children: ["v", props.version]
1262
+ }) : null]
1192
1263
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
1264
  className: settings_card_module_css_default.description,
1194
1265
  children: props.t(props.descriptionKey)
@@ -1625,6 +1696,13 @@ window.__ModuleLoader__.load({
1625
1696
  };
1626
1697
  //#endregion
1627
1698
  //#region src/client/SkillHubSettingsCard.tsx
1699
+ /**
1700
+ * The dsh-skill-hub plugin settings card: bridges the hub's settings
1701
+ * namespace (bound through the official settings transport) onto the
1702
+ * family-style staged card form (enabled master switch + agent announcement).
1703
+ * Registered into the official `settings.plugin.item` slot keyed by that
1704
+ * namespace, so the plugin shows up in Settings → 插件 on dsh rc.7+.
1705
+ */
1628
1706
  /** Bridges the hub's config scope onto the card's staged form. */
1629
1707
  var SkillHubSettingsCardController = class {
1630
1708
  form;
@@ -1683,6 +1761,16 @@ window.__ModuleLoader__.load({
1683
1761
  function SkillHubSettingsCard(props) {
1684
1762
  const { t } = props;
1685
1763
  const state = props.useSkillHubSettingsCard((snapshot) => snapshot);
1764
+ const [version, setVersion] = (0, react.useState)(void 0);
1765
+ (0, react.useEffect)(() => {
1766
+ let cancelled = false;
1767
+ new SkillHubApi().config().then((res) => {
1768
+ if (!cancelled && res.ok) setVersion(res.pluginVersion);
1769
+ }).catch(() => {});
1770
+ return () => {
1771
+ cancelled = true;
1772
+ };
1773
+ }, []);
1686
1774
  const disabled = !state.writable;
1687
1775
  const fieldProps = {
1688
1776
  overriddenLabel: t("settings.overridden"),
@@ -1693,6 +1781,7 @@ window.__ModuleLoader__.load({
1693
1781
  t,
1694
1782
  titleKey: "settings.title",
1695
1783
  descriptionKey: "settings.description",
1784
+ version,
1696
1785
  state,
1697
1786
  onSave: props.save,
1698
1787
  onDiscard: props.discard,
@@ -1824,7 +1913,7 @@ window.__ModuleLoader__.load({
1824
1913
  }
1825
1914
  //#endregion
1826
1915
  //#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/panel/panel.module.css.mjs
1827
- const css = "._6VtqdG_panel{-webkit-font-smoothing:antialiased;color-scheme:light dark;--hub-model:#2f81f7;--hub-user:#3fb950;flex-direction:column;gap:14px;max-width:720px;margin:0 auto;padding:28px 20px 44px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,sans-serif;display:flex}._6VtqdG_header{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}._6VtqdG_title{letter-spacing:-.02em;margin:0;font-size:24px;font-weight:700}._6VtqdG_hint{opacity:.5;font-size:12px}._6VtqdG_actions{gap:8px;margin-left:auto;display:flex}._6VtqdG_segmented{background:#80808024;border-radius:9px;gap:2px;padding:2px;display:flex}._6VtqdG_segBtn{color:inherit;opacity:.62;cursor:pointer;background:0 0;border:none;border-radius:7px;padding:5px 13px;font-size:12px;transition:background .15s,opacity .15s}._6VtqdG_segBtn:hover{opacity:.9}._6VtqdG_segBtnActive{opacity:1;background:#80808047;font-weight:600}._6VtqdG_search{width:100%;color:inherit;background:#8080801f;border:none;border-radius:10px;outline:none;padding:10px 14px;font-size:13px;transition:background .15s}._6VtqdG_search::placeholder{opacity:.45}._6VtqdG_search:focus{background:#8080802e}._6VtqdG_section{background:#8080800f;border-radius:12px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_sectionTitle{margin:14px 16px 2px;font-size:13px;font-weight:700}._6VtqdG_groupHead{align-items:center;gap:8px;padding:12px 14px 4px;display:flex}._6VtqdG_groupTitle{opacity:.92;flex:1;min-width:0;font-size:13px;font-weight:600}._6VtqdG_groupOps{flex:none;gap:6px;display:inline-flex}._6VtqdG_opBtn{color:inherit;cursor:pointer;background:#80808021;border:none;border-radius:999px;padding:3px 11px;font-size:11px;transition:background .15s}._6VtqdG_opBtn:hover{background:#80808038}._6VtqdG_opBtn:disabled{opacity:.4;cursor:default}._6VtqdG_row{cursor:pointer;background:0 0;align-items:center;gap:12px;padding:10px 14px;transition:background .12s;display:flex}._6VtqdG_row:hover{background:#80808014}._6VtqdG_row:focus-visible{outline:2px solid var(--hub-model);outline-offset:-2px}._6VtqdG_rowStatic{cursor:default}._6VtqdG_rowStatic:hover{background:0 0}._6VtqdG_row+._6VtqdG_row{border-top:.5px solid #80808021}._6VtqdG_rowMain{flex:1;min-width:0}._6VtqdG_rowName{align-items:center;gap:5px;font-size:13.5px;display:flex}._6VtqdG_rowNameText{font-weight:600}._6VtqdG_rowDesc{opacity:.52;white-space:nowrap;text-overflow:ellipsis;margin-top:1px;font-size:12.5px;overflow:hidden}._6VtqdG_rowMeta{opacity:.45;white-space:nowrap;flex:none;font-size:11.5px}._6VtqdG_badges{flex:none;align-items:center;gap:6px;display:flex}._6VtqdG_badge{opacity:.8;white-space:nowrap;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_badgeModel{color:var(--hub-model);border-color:currentColor}._6VtqdG_badgeUser{color:var(--hub-user);border-color:currentColor}._6VtqdG_badgeUses{color:#d29922;border-color:currentColor}._6VtqdG_badgeReadonly{opacity:.5}._6VtqdG_switch{cursor:pointer;background:#8080804d;border:none;border-radius:999px;flex:none;width:36px;height:21px;padding:2px;transition:background .18s}._6VtqdG_switch:disabled{opacity:.5;cursor:default}._6VtqdG_switchOn{background:#34c759}._6VtqdG_switchThumb{background:#fff;border-radius:50%;width:17px;height:17px;transition:transform .18s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}._6VtqdG_switchOn ._6VtqdG_switchThumb{transform:translate(15px)}._6VtqdG_empty{opacity:.5;text-align:center;padding:20px 0;font-size:13px}._6VtqdG_diagRow{background:#d299220f;border-left:3px solid #d29922;padding:9px 14px;font-size:12px}._6VtqdG_diagPath{opacity:.75;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._6VtqdG_diagReason{opacity:.65;margin-top:1px}._6VtqdG_updateLink{color:inherit;font-weight:600;text-decoration:underline}._6VtqdG_errorBanner{color:#d1242f;white-space:pre-wrap;word-break:break-all;background:#d1242f12;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_successBanner{color:#3fb950;white-space:pre-wrap;word-break:break-all;background:#3fb95014;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_detailHead{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._6VtqdG_back{color:inherit;cursor:pointer;background:#8080801f;border:none;border-radius:8px;padding:6px 12px;font-size:12.5px}._6VtqdG_back:hover{background:#80808033}._6VtqdG_detailName{letter-spacing:-.01em;font-size:19px;font-weight:700}._6VtqdG_detailMeta{opacity:.6;flex-direction:column;gap:4px;font-size:12px;display:flex}._6VtqdG_detailMetaLine{word-break:break-all}._6VtqdG_detailContent{white-space:pre-wrap;word-break:break-word;background:#8080800d;border-radius:12px;max-height:62vh;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;overflow:auto}._6VtqdG_form{background:#8080800d;border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_formRow{flex-direction:column;gap:5px;display:flex}._6VtqdG_formLabel{opacity:.6;font-size:12px}._6VtqdG_input,._6VtqdG_select{color:inherit;background:#8080801f;border:none;border-radius:9px;outline:none;padding:9px 12px;font-size:13px}._6VtqdG_input:focus,._6VtqdG_select:focus{background:#8080802e}._6VtqdG_buttons{align-items:center;gap:8px;display:flex}._6VtqdG_button{cursor:pointer;color:inherit;background:#8080801f;border:none;border-radius:8px;padding:6px 13px;font-size:12.5px;transition:background .15s}._6VtqdG_button:hover{background:#80808033}._6VtqdG_primary{background:var(--hub-model);color:#fff}._6VtqdG_primary:hover{opacity:.92;background:#2f81f7}._6VtqdG_primary:disabled{opacity:.45;cursor:default}._6VtqdG_danger{color:#fff;background:#d1242f}._6VtqdG_danger:hover{opacity:.92;background:#d1242f}._6VtqdG_danger:disabled{opacity:.45;cursor:default}._6VtqdG_formError{color:#d1242f}._6VtqdG_formSuccess{color:#3fb950}._6VtqdG_muted{opacity:.55}._6VtqdG_dot{vertical-align:middle;border-radius:50%;width:7px;height:7px;margin-left:5px;display:inline-block}._6VtqdG_dotModel{background:var(--hub-model)}._6VtqdG_dotUser{background:var(--hub-user)}._6VtqdG_legend{opacity:.55;flex-wrap:wrap;align-items:center;gap:12px;padding:2px 2px 0;font-size:11.5px;display:flex}._6VtqdG_legendItem{align-items:center;gap:4px;display:inline-flex}._6VtqdG_legendItem ._6VtqdG_dot{margin-left:0}._6VtqdG_legendHint{opacity:.8}._6VtqdG_badgeSource,._6VtqdG_badgeCount{opacity:.72;background:#8080801a;border:none}._6VtqdG_badgeDisabled{opacity:.58;background:#8080801a;border:none}._6VtqdG_disclosure{min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:7px;padding:2px 0;display:flex}._6VtqdG_disclosure:hover ._6VtqdG_groupTitle{opacity:1}._6VtqdG_chevron{opacity:.55;border-bottom:1.5px solid;border-right:1.5px solid;flex:none;width:8px;height:8px;margin-right:2px;transition:transform .15s;transform:rotate(45deg)}._6VtqdG_chevronCollapsed{transform:rotate(-45deg)}._6VtqdG_headerCount{opacity:.5;white-space:nowrap;margin-left:2px;font-size:12px}._6VtqdG_pluginVersion{opacity:.45;white-space:nowrap;font-variant-numeric:tabular-nums;margin-left:6px;font-size:12px;font-weight:500}._6VtqdG_subbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_legendToggle{width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:#80808024;border:none;border-radius:50%;flex:none;font-size:12px;line-height:1;transition:opacity .15s,background .15s}._6VtqdG_legendToggle:hover{opacity:1}._6VtqdG_legendToggleActive{opacity:1;background:#80808047}._6VtqdG_filterBar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_formLabel{margin:0}._6VtqdG_useCount{color:#d29922;vertical-align:1px;background:#d299221f;border-radius:999px;margin-left:6px;padding:0 6px;font-size:11px;font-weight:700;line-height:1.6;display:inline-block}._6VtqdG_useTime{opacity:.45;white-space:nowrap;margin-left:auto;font-size:11px}._6VtqdG_switchMixed{background:#2f81f773}._6VtqdG_switchMixed ._6VtqdG_switchThumb{transform:translate(7.5px);box-shadow:0 1px 2px #00000040}._6VtqdG_groupTitleInner{align-items:baseline;display:inline-flex}._6VtqdG_groupOps{flex-wrap:wrap;flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_sourceLink{color:inherit;opacity:.9;border-bottom:1px dotted;font-weight:600;text-decoration:none}._6VtqdG_sourceLink:hover{opacity:1}._6VtqdG_statusBadges{flex:none;align-items:center;gap:4px;display:inline-flex}._6VtqdG_statusBadge{opacity:.8;white-space:nowrap;background:#8080801a;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_statusOk{color:#3fb950;background:0 0;border-color:currentColor}._6VtqdG_statusUpdated{color:#d29922;background:#d2992214;border-color:currentColor}._6VtqdG_statusError{color:#d1242f;background:#d1242f14;border-color:currentColor}._6VtqdG_statusWrap{cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-family:inherit;display:inline-flex}._6VtqdG_statusWrap:hover ._6VtqdG_statusBadge{opacity:1}._6VtqdG_statusButton{cursor:pointer;font-family:inherit}._6VtqdG_statusButton:hover{opacity:1}._6VtqdG_opDanger{color:#d1242f;background:#d1242f1a}._6VtqdG_opDanger:hover{background:#d1242f2e}._6VtqdG_iconBtn{border-radius:999px;justify-content:center;align-items:center;width:24px;height:24px;padding:0;display:inline-flex}._6VtqdG_sourceCard{background:#8080800d;border-radius:12px;flex-direction:column;gap:4px;padding:12px 14px;font-size:12px;display:flex}._6VtqdG_sourceCardTitle{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_dialogOverlay{z-index:50;-webkit-backdrop-filter:blur(2px);background:#00000059;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}._6VtqdG_dialog{background:var(--dsw-surface,#f5f5f7);min-width:min(320px,100%);max-width:380px;color:var(--dsw-text,#1d1d1f);border-radius:14px;padding:18px 18px 14px;box-shadow:0 12px 40px #00000047,0 2px 8px #00000029}@media (prefers-color-scheme:dark){._6VtqdG_dialog{color:#f5f5f7;background:#2c2c2e}}._6VtqdG_dialogTitle{letter-spacing:-.01em;margin:0 0 8px;font-size:15px;font-weight:700}._6VtqdG_dialogText{opacity:.65;margin:0 0 10px;font-size:12.5px;line-height:1.5}._6VtqdG_dialogList{flex-direction:column;gap:5px;max-height:200px;margin:0 0 14px;padding:0;list-style:none;display:flex;overflow:auto}._6VtqdG_dialogList li{opacity:.85;word-break:break-all;font-size:12.5px}._6VtqdG_dialogActions{justify-content:flex-end;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_search{flex:1;min-width:160px}._6VtqdG_filterBar ._6VtqdG_formLabel{flex:none}._6VtqdG_filterBar ._6VtqdG_select{flex:none;max-width:150px}._6VtqdG_filterBar ._6VtqdG_segmented{flex:none}._6VtqdG_titleIcon{vertical-align:-2px;opacity:.8;margin-right:2px;display:inline-block}._6VtqdG_grow{flex:1}._6VtqdG_hintLine{opacity:.55;margin:0;font-size:11.5px}._6VtqdG_hintPadded{margin:6px 12px 2px}._6VtqdG_hintInline{margin:4px 2px}._6VtqdG_rowMuted{cursor:default;opacity:.55}._6VtqdG_actionsPadded{padding:8px 12px}._6VtqdG_actionsTop{margin-top:8px}._6VtqdG_actionsBottom{margin-bottom:8px}._6VtqdG_groupTime{margin-left:6px}._6VtqdG_sectionHeadRow{align-items:center;gap:8px;display:flex}._6VtqdG_sectionTitleFill{flex:1}._6VtqdG_dialogSelect{width:100%;margin-bottom:12px}._6VtqdG_dialogRow{align-items:center;gap:8px;display:flex}._6VtqdG_workspaceBox{align-items:center;gap:6px;display:inline-flex}._6VtqdG_workspaceInput{width:200px;padding:6px 10px;font-size:12px}._6VtqdG_projectNest{border-left:1px solid #80808024;flex-direction:column;margin:2px 0 0 14px;display:flex}";
1916
+ const css = "._6VtqdG_panel{-webkit-font-smoothing:antialiased;color-scheme:light dark;--hub-model:#2f81f7;--hub-user:#3fb950;flex-direction:column;gap:14px;max-width:720px;margin:0 auto;padding:28px 20px 44px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,sans-serif;display:flex}._6VtqdG_header{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}._6VtqdG_title{letter-spacing:-.02em;margin:0;font-size:24px;font-weight:700}._6VtqdG_hint{opacity:.5;font-size:12px}._6VtqdG_actions{gap:8px;margin-left:auto;display:flex}._6VtqdG_segmented{background:#80808024;border-radius:9px;gap:2px;padding:2px;display:flex}._6VtqdG_segBtn{color:inherit;opacity:.62;cursor:pointer;background:0 0;border:none;border-radius:7px;padding:5px 13px;font-size:12px;transition:background .15s,opacity .15s}._6VtqdG_segBtn:hover{opacity:.9}._6VtqdG_segBtnActive{opacity:1;background:#80808047;font-weight:600}._6VtqdG_search{width:100%;color:inherit;background:#8080801f;border:none;border-radius:10px;outline:none;padding:10px 14px;font-size:13px;transition:background .15s}._6VtqdG_search::placeholder{opacity:.45}._6VtqdG_search:focus{background:#8080802e}._6VtqdG_section{background:#8080800f;border-radius:12px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_sectionTitle{margin:14px 16px 2px;font-size:13px;font-weight:700}._6VtqdG_groupHead{box-sizing:border-box;align-items:center;gap:8px;min-height:44px;padding:10px 14px;display:flex}._6VtqdG_groupTitle{opacity:.92;flex:1;align-items:center;gap:6px;min-width:0;font-size:13px;font-weight:600;line-height:1;display:inline-flex}._6VtqdG_groupOps{flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_opBtn{color:inherit;cursor:pointer;background:#80808021;border:none;border-radius:999px;padding:3px 11px;font-size:11px;transition:background .15s}._6VtqdG_opBtn:hover{background:#80808038}._6VtqdG_opBtn:disabled{opacity:.4;cursor:default}._6VtqdG_row{cursor:pointer;background:0 0;align-items:center;gap:12px;padding:10px 14px;transition:background .12s;display:flex}._6VtqdG_row:hover{background:#80808014}._6VtqdG_row:focus-visible{outline:2px solid var(--hub-model);outline-offset:-2px}._6VtqdG_rowStatic{cursor:default}._6VtqdG_rowStatic:hover{background:0 0}._6VtqdG_row+._6VtqdG_row{border-top:.5px solid #80808021}._6VtqdG_rowMain{flex:1;min-width:0}._6VtqdG_rowName{align-items:center;gap:5px;font-size:13.5px;display:flex}._6VtqdG_rowNameText{font-weight:600}._6VtqdG_rowDesc{opacity:.52;white-space:nowrap;text-overflow:ellipsis;margin-top:1px;font-size:12.5px;overflow:hidden}._6VtqdG_rowMeta{opacity:.45;white-space:nowrap;flex:none;font-size:11.5px}._6VtqdG_badges{flex:none;align-items:center;gap:6px;display:flex}._6VtqdG_badge{opacity:.8;white-space:nowrap;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_badgeModel{color:var(--hub-model);border-color:currentColor}._6VtqdG_badgeUser{color:var(--hub-user);border-color:currentColor}._6VtqdG_badgeUses{color:#d29922;border-color:currentColor}._6VtqdG_badgeReadonly{opacity:.5}._6VtqdG_switch{cursor:pointer;background:#8080804d;border:none;border-radius:999px;flex:none;width:36px;height:21px;padding:2px;transition:background .18s}._6VtqdG_switch:disabled{opacity:.5;cursor:default}._6VtqdG_switchOn{background:#34c759}._6VtqdG_switchThumb{background:#fff;border-radius:50%;width:17px;height:17px;transition:transform .18s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}._6VtqdG_switchOn ._6VtqdG_switchThumb{transform:translate(15px)}._6VtqdG_empty{opacity:.5;text-align:center;padding:20px 0;font-size:13px}._6VtqdG_diagRow{background:#d299220f;border-left:3px solid #d29922;padding:9px 14px;font-size:12px}._6VtqdG_diagPath{opacity:.75;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._6VtqdG_diagReason{opacity:.65;margin-top:1px}._6VtqdG_updateLink{color:inherit;font-weight:600;text-decoration:underline}._6VtqdG_errorBanner{color:#d1242f;white-space:pre-wrap;word-break:break-all;background:#d1242f12;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_successBanner{color:#3fb950;white-space:pre-wrap;word-break:break-all;background:#3fb95014;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_detailHead{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._6VtqdG_back{color:inherit;cursor:pointer;background:#8080801f;border:none;border-radius:8px;padding:6px 12px;font-size:12.5px}._6VtqdG_back:hover{background:#80808033}._6VtqdG_detailName{letter-spacing:-.01em;font-size:19px;font-weight:700}._6VtqdG_detailMeta{opacity:.6;flex-direction:column;gap:4px;font-size:12px;display:flex}._6VtqdG_detailMetaLine{word-break:break-all}._6VtqdG_detailContent{white-space:pre-wrap;word-break:break-word;background:#8080800d;border-radius:12px;max-height:62vh;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;overflow:auto}._6VtqdG_form{background:#8080800d;border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_formRow{flex-direction:column;gap:5px;display:flex}._6VtqdG_formLabel{opacity:.6;font-size:12px}._6VtqdG_input,._6VtqdG_select{color:inherit;background:#8080801f;border:none;border-radius:9px;outline:none;padding:9px 12px;font-size:13px}._6VtqdG_input:focus,._6VtqdG_select:focus{background:#8080802e}._6VtqdG_buttons{align-items:center;gap:8px;display:flex}._6VtqdG_button{cursor:pointer;color:inherit;background:#8080801f;border:none;border-radius:8px;padding:6px 13px;font-size:12.5px;transition:background .15s}._6VtqdG_button:hover{background:#80808033}._6VtqdG_primary{background:var(--hub-model);color:#fff}._6VtqdG_primary:hover{opacity:.92;background:#2f81f7}._6VtqdG_primary:disabled{opacity:.45;cursor:default}._6VtqdG_danger{color:#fff;background:#d1242f}._6VtqdG_danger:hover{opacity:.92;background:#d1242f}._6VtqdG_danger:disabled{opacity:.45;cursor:default}._6VtqdG_formError{color:#d1242f}._6VtqdG_formSuccess{color:#3fb950}._6VtqdG_muted{opacity:.55}._6VtqdG_dot{vertical-align:middle;border-radius:50%;width:7px;height:7px;margin-left:5px;display:inline-block}._6VtqdG_dotModel{background:var(--hub-model)}._6VtqdG_dotUser{background:var(--hub-user)}._6VtqdG_legend{opacity:.55;flex-wrap:wrap;align-items:center;gap:12px;padding:2px 2px 0;font-size:11.5px;display:flex}._6VtqdG_legendItem{align-items:center;gap:4px;display:inline-flex}._6VtqdG_legendItem ._6VtqdG_dot{margin-left:0}._6VtqdG_legendHint{opacity:.8}._6VtqdG_badgeSource,._6VtqdG_badgeCount{opacity:.72;background:#8080801a;border:none}._6VtqdG_badgeDisabled{opacity:.58;background:#8080801a;border:none}._6VtqdG_disclosure{min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:7px;min-height:24px;padding:2px 0;display:flex}._6VtqdG_disclosure:hover ._6VtqdG_groupTitle{opacity:1}._6VtqdG_chevron{opacity:.55;border-bottom:1.5px solid;border-right:1.5px solid;flex:none;align-self:center;width:8px;height:8px;margin-right:2px;transition:transform .15s;transform:rotate(45deg)translateY(-1px)}._6VtqdG_chevronCollapsed{transform:rotate(-45deg)translateY(-1px)}._6VtqdG_headerCount{opacity:.5;white-space:nowrap;margin-left:2px;font-size:12px}._6VtqdG_pluginVersion{opacity:.45;white-space:nowrap;font-variant-numeric:tabular-nums;margin-left:6px;font-size:12px;font-weight:500}._6VtqdG_subbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_legendToggle{width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:#80808024;border:none;border-radius:50%;flex:none;font-size:12px;line-height:1;transition:opacity .15s,background .15s}._6VtqdG_legendToggle:hover{opacity:1}._6VtqdG_legendToggleActive{opacity:1;background:#80808047}._6VtqdG_filterBar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_formLabel{margin:0}._6VtqdG_useCount{color:#d29922;vertical-align:1px;background:#d299221f;border-radius:999px;margin-left:6px;padding:0 6px;font-size:11px;font-weight:700;line-height:1.6;display:inline-block}._6VtqdG_useTime{opacity:.45;white-space:nowrap;margin-left:auto;font-size:11px}._6VtqdG_switchMixed{background:#2f81f773}._6VtqdG_switchMixed ._6VtqdG_switchThumb{transform:translate(7.5px);box-shadow:0 1px 2px #00000040}._6VtqdG_groupTitleInner{align-items:baseline;display:inline-flex}._6VtqdG_groupOps{flex-wrap:wrap;flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_sourceLink{color:inherit;opacity:.9;border-bottom:1px dotted;font-weight:600;text-decoration:none}._6VtqdG_sourceLink:hover{opacity:1}._6VtqdG_statusBadges{flex:none;align-items:center;gap:4px;display:inline-flex}._6VtqdG_statusBadge{opacity:.8;white-space:nowrap;background:#8080801a;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_statusOk{color:#3fb950;background:0 0;border-color:currentColor}._6VtqdG_statusUpdated{color:#d29922;background:#d2992214;border-color:currentColor}._6VtqdG_statusError{color:#d1242f;background:#d1242f14;border-color:currentColor}._6VtqdG_statusWrap{cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-family:inherit;display:inline-flex}._6VtqdG_statusWrap:hover ._6VtqdG_statusBadge{opacity:1}._6VtqdG_statusButton{cursor:pointer;font-family:inherit}._6VtqdG_statusButton:hover{opacity:1}._6VtqdG_opDanger{color:#d1242f;background:#d1242f1a}._6VtqdG_opDanger:hover{background:#d1242f2e}._6VtqdG_iconBtn{border-radius:999px;justify-content:center;align-items:center;width:24px;height:24px;padding:0;display:inline-flex}._6VtqdG_sourceCard{background:#8080800d;border-radius:12px;flex-direction:column;gap:4px;padding:12px 14px;font-size:12px;display:flex}._6VtqdG_sourceCardTitle{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_dialogOverlay{z-index:50;-webkit-backdrop-filter:blur(2px);background:#00000059;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}._6VtqdG_dialog{background:var(--dsw-surface,#f5f5f7);min-width:min(320px,100%);max-width:380px;color:var(--dsw-text,#1d1d1f);border-radius:14px;padding:18px 18px 14px;box-shadow:0 12px 40px #00000047,0 2px 8px #00000029}@media (prefers-color-scheme:dark){._6VtqdG_dialog{color:#f5f5f7;background:#2c2c2e}}._6VtqdG_dialogTitle{letter-spacing:-.01em;margin:0 0 8px;font-size:15px;font-weight:700}._6VtqdG_dialogText{opacity:.65;margin:0 0 10px;font-size:12.5px;line-height:1.5}._6VtqdG_dialogList{flex-direction:column;gap:5px;max-height:200px;margin:0 0 14px;padding:0;list-style:none;display:flex;overflow:auto}._6VtqdG_dialogList li{opacity:.85;word-break:break-all;font-size:12.5px}._6VtqdG_dialogActions{justify-content:flex-end;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_search{flex:1;min-width:160px}._6VtqdG_filterBar ._6VtqdG_formLabel{flex:none}._6VtqdG_filterBar ._6VtqdG_select{flex:none;max-width:150px}._6VtqdG_filterBar ._6VtqdG_segmented{flex:none}._6VtqdG_titleIcon{vertical-align:-2px;opacity:.8;margin-right:2px;display:inline-block}._6VtqdG_grow{flex:1}._6VtqdG_hintLine{opacity:.55;margin:0;font-size:11.5px}._6VtqdG_hintPadded{margin:6px 12px 2px}._6VtqdG_hintInline{margin:4px 2px}._6VtqdG_rowMuted{cursor:default;opacity:.55}._6VtqdG_actionsPadded{padding:8px 12px}._6VtqdG_actionsTop{margin-top:8px}._6VtqdG_actionsBottom{margin-bottom:8px}._6VtqdG_groupTime{margin-left:6px}._6VtqdG_sectionHeadRow{align-items:center;gap:8px;display:flex}._6VtqdG_sectionTitleFill{flex:1}._6VtqdG_dialogSelect{width:100%;margin-bottom:12px}._6VtqdG_dialogRow{align-items:center;gap:8px;display:flex}._6VtqdG_workspaceBox{align-items:center;gap:6px;display:inline-flex}._6VtqdG_workspaceInput{width:200px;padding:6px 10px;font-size:12px}._6VtqdG_projectNest{border-left:1px solid #80808024;flex-direction:column;margin:2px 0 0 14px;display:flex}._6VtqdG_dragHandle{cursor:grab;opacity:.32;letter-spacing:1px;user-select:none;flex:none;justify-content:center;align-self:center;align-items:center;height:20px;padding:0 4px;font-size:11px;display:inline-flex}._6VtqdG_dragHandle:active{cursor:grabbing}._6VtqdG_dragging{opacity:.45}._6VtqdG_dragOver{outline:2px dashed var(--hub-model,#2f81f7);outline-offset:-2px;background:#2f81f712}._6VtqdG_scanList{background:#8080800f;border:.5px solid #80808021;border-radius:12px;max-height:360px;margin-top:8px;overflow:auto}._6VtqdG_scanProgressTrack{background:#80808021;border-radius:4px;flex:1;height:6px;overflow:hidden}._6VtqdG_scanProgressFill{background:var(--hub-model,#2f81f7);height:100%;transition:width .2s}";
1828
1917
  const tagId = "dsh-skill-hub/panel.module.css";
1829
1918
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1830
1919
  const tag = document.createElement("style");
@@ -1873,6 +1962,9 @@ window.__ModuleLoader__.load({
1873
1962
  "dot": "_6VtqdG_dot",
1874
1963
  "dotModel": "_6VtqdG_dotModel",
1875
1964
  "dotUser": "_6VtqdG_dotUser",
1965
+ "dragHandle": "_6VtqdG_dragHandle",
1966
+ "dragOver": "_6VtqdG_dragOver",
1967
+ "dragging": "_6VtqdG_dragging",
1876
1968
  "empty": "_6VtqdG_empty",
1877
1969
  "errorBanner": "_6VtqdG_errorBanner",
1878
1970
  "filterBar": "_6VtqdG_filterBar",
@@ -1915,6 +2007,9 @@ window.__ModuleLoader__.load({
1915
2007
  "rowName": "_6VtqdG_rowName",
1916
2008
  "rowNameText": "_6VtqdG_rowNameText",
1917
2009
  "rowStatic": "_6VtqdG_rowStatic",
2010
+ "scanList": "_6VtqdG_scanList",
2011
+ "scanProgressFill": "_6VtqdG_scanProgressFill",
2012
+ "scanProgressTrack": "_6VtqdG_scanProgressTrack",
1918
2013
  "search": "_6VtqdG_search",
1919
2014
  "section": "_6VtqdG_section",
1920
2015
  "sectionHeadRow": "_6VtqdG_sectionHeadRow",
@@ -2347,17 +2442,6 @@ window.__ModuleLoader__.load({
2347
2442
  children: skill.description
2348
2443
  })]
2349
2444
  }), skill.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2350
- type: "button",
2351
- className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger + " " + panel_module_css_default.iconBtn,
2352
- disabled: hub.busyNames.has(skill.name) || hub.tagBusy,
2353
- title: tt("row.delete"),
2354
- "aria-label": tt("row.delete"),
2355
- onClick: (event) => {
2356
- event.stopPropagation();
2357
- hub.requestDeleteSkill(skill.name);
2358
- },
2359
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutline16, { size: 14 })
2360
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2361
2445
  type: "button",
2362
2446
  role: "switch",
2363
2447
  "aria-checked": true,
@@ -2369,7 +2453,18 @@ window.__ModuleLoader__.load({
2369
2453
  hub.toggle(skill, false);
2370
2454
  },
2371
2455
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2372
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2456
+ }), hub.editMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2457
+ type: "button",
2458
+ className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger + " " + panel_module_css_default.iconBtn,
2459
+ disabled: hub.busyNames.has(skill.name) || hub.tagBusy,
2460
+ title: tt("row.delete"),
2461
+ "aria-label": tt("row.delete"),
2462
+ onClick: (event) => {
2463
+ event.stopPropagation();
2464
+ hub.requestDeleteSkill(skill.name);
2465
+ },
2466
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutline16, { size: 14 })
2467
+ }) : null] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2373
2468
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
2374
2469
  children: tt("row.readonly")
2375
2470
  })]
@@ -2441,219 +2536,326 @@ window.__ModuleLoader__.load({
2441
2536
  }
2442
2537
  //#endregion
2443
2538
  //#region src/client/panel/SourcesView.tsx
2444
- /** 项目级三级树:项目级 → 具体工作区(可折叠、可细分)→ .dsh/.agents。 */
2445
- function ProjectTree(props) {
2539
+ /**
2540
+ * Sources tab: the flat skill list or the grouped view — a project-level
2541
+ * three-tier tree (workspaces from workspace.json, each optionally split by
2542
+ * .dsh/.agents), one card per upstream collection with check/sync/
2543
+ * follow-delete actions and the tri-state switch, plus the uncategorized
2544
+ * "personal" card (project skills never count as personal).
2545
+ */
2546
+ function SourcesView(props) {
2446
2547
  const { hub } = props;
2447
- const { sorted, sourceFilter, origins, collapsedGroups, subdividedProjects, toggleGroupCollapse, toggleSubdivide } = hub;
2448
- const projectSkills = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
2449
- if (projectSkills.length === 0) return null;
2450
- const byProject = /* @__PURE__ */ new Map();
2451
- for (const skill of projectSkills) {
2452
- const key = skill.workspace ?? skill.source;
2453
- const entry = byProject.get(key);
2454
- if (entry === void 0) byProject.set(key, {
2455
- title: skill.workspaceTitle ?? skill.workspace ?? tt("groups.project"),
2456
- skills: [skill]
2457
- });
2458
- else entry.skills.push(skill);
2459
- }
2460
- const topCollapsed = collapsedGroups.has("project");
2461
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2462
- className: panel_module_css_default.section,
2463
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2464
- className: panel_module_css_default.groupHead,
2465
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2466
- type: "button",
2467
- className: panel_module_css_default.disclosure,
2468
- "aria-expanded": !topCollapsed,
2469
- onClick: () => {
2470
- toggleGroupCollapse("project");
2548
+ const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, requestDeleteGroup, toggleGroup, enableDisabled } = hub;
2549
+ const [topDragKey, setTopDragKey] = (0, react.useState)(null);
2550
+ const [topOverKey, setTopOverKey] = (0, react.useState)(null);
2551
+ 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, {
2552
+ skill,
2553
+ hub
2554
+ }, skill.name)) });
2555
+ const projectSkillsAll = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
2556
+ const hasProject = projectSkillsAll.length > 0;
2557
+ const collections = groupsState?.collections ?? [];
2558
+ const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
2559
+ const personalDisabledAll = (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");
2560
+ const hasPersonal = [...uncategorized.map((s) => s.name), ...personalDisabledAll.map((r) => r.name)].length > 0;
2561
+ const defaultTopKeys = [
2562
+ ...hasProject ? ["project"] : [],
2563
+ ...collections.map((c) => "col:" + c.name),
2564
+ ...hasPersonal ? ["uncategorized-source"] : []
2565
+ ];
2566
+ const storedTopOrder = groupsState?.sourceGroupOrder ?? [];
2567
+ const topOrderedKeys = (() => {
2568
+ if (storedTopOrder.length === 0) return defaultTopKeys;
2569
+ const set = new Set(storedTopOrder);
2570
+ const result = storedTopOrder.filter((k) => defaultTopKeys.includes(k));
2571
+ for (const k of defaultTopKeys) if (!set.has(k)) result.push(k);
2572
+ if (result.length === 0) return defaultTopKeys;
2573
+ return result;
2574
+ })();
2575
+ const handleTopDrop = (targetKey) => {
2576
+ if (topDragKey === null || topDragKey === targetKey) return;
2577
+ const from = topOrderedKeys.indexOf(topDragKey);
2578
+ const to = topOrderedKeys.indexOf(targetKey);
2579
+ if (from === -1 || to === -1) return;
2580
+ const next = [...topOrderedKeys];
2581
+ const [moved] = next.splice(from, 1);
2582
+ next.splice(to, 0, moved);
2583
+ hub.reorderSourceGroups(next);
2584
+ };
2585
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!hasProject && collections.length === 0 && !hasPersonal ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2586
+ className: panel_module_css_default.empty,
2587
+ children: tt("groups.noCollections")
2588
+ }) : null, topOrderedKeys.map((topKey) => {
2589
+ if (topKey === "project" && hasProject) {
2590
+ const topCollapsed = collapsedGroups.has("project");
2591
+ const isDragging = topDragKey === "project";
2592
+ const isOver = topOverKey === "project" && topDragKey !== "project";
2593
+ const byProject = /* @__PURE__ */ new Map();
2594
+ for (const skill of projectSkillsAll) {
2595
+ const key = skill.workspace ?? skill.source;
2596
+ const entry = byProject.get(key);
2597
+ if (entry === void 0) byProject.set(key, {
2598
+ title: skill.workspaceTitle ?? skill.workspace ?? tt("groups.project"),
2599
+ skills: [skill]
2600
+ });
2601
+ else entry.skills.push(skill);
2602
+ }
2603
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2604
+ className: panel_module_css_default.section + (isDragging ? " " + panel_module_css_default.dragging : "") + (isOver ? " " + panel_module_css_default.dragOver : ""),
2605
+ draggable: true,
2606
+ onDragStart: (e) => {
2607
+ setTopDragKey("project");
2608
+ e.dataTransfer.effectAllowed = "move";
2609
+ e.dataTransfer.setData("text/plain", "project");
2610
+ },
2611
+ onDragOver: (e) => {
2612
+ e.preventDefault();
2613
+ if (topOverKey !== "project") setTopOverKey("project");
2614
+ },
2615
+ onDragLeave: () => {
2616
+ if (topOverKey === "project") setTopOverKey(null);
2617
+ },
2618
+ onDrop: (e) => {
2619
+ e.preventDefault();
2620
+ handleTopDrop("project");
2621
+ setTopOverKey(null);
2622
+ },
2623
+ onDragEnd: () => {
2624
+ setTopDragKey(null);
2625
+ setTopOverKey(null);
2471
2626
  },
2472
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (topCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2473
- className: panel_module_css_default.groupTitle,
2474
- children: [
2475
- tt("groups.project"),
2476
- " · ",
2477
- byProject.size
2478
- ]
2479
- })]
2480
- })
2481
- }), !topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
2482
- const projKey = "project:" + key;
2483
- const projCollapsed = collapsedGroups.has(projKey);
2484
- const subdivided = subdividedProjects.has(key);
2485
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2486
- className: panel_module_css_default.projectNest,
2487
2627
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2488
2628
  className: panel_module_css_default.groupHead,
2489
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2629
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2630
+ className: panel_module_css_default.dragHandle,
2631
+ "aria-hidden": true,
2632
+ title: "拖拽调整顺序",
2633
+ children: "⋮⋮"
2634
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2490
2635
  type: "button",
2491
2636
  className: panel_module_css_default.disclosure,
2492
- "aria-expanded": !projCollapsed,
2637
+ "aria-expanded": !topCollapsed,
2493
2638
  onClick: () => {
2494
- toggleGroupCollapse(projKey);
2639
+ toggleGroupCollapse("project");
2495
2640
  },
2496
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (projCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2641
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (topCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2497
2642
  className: panel_module_css_default.groupTitle,
2498
2643
  children: [
2499
- proj.title,
2644
+ tt("groups.project"),
2500
2645
  " · ",
2501
- proj.skills.length,
2502
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2503
- members: proj.skills.map((skill) => skill.name),
2504
- hub
2505
- })
2646
+ byProject.size
2506
2647
  ]
2507
2648
  })]
2508
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2509
- className: panel_module_css_default.groupOps,
2510
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2511
- type: "button",
2512
- className: panel_module_css_default.opBtn,
2513
- onClick: (event) => {
2514
- event.stopPropagation();
2515
- toggleSubdivide(key);
2516
- },
2517
- children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2518
- })
2519
2649
  })]
2520
- }), !projCollapsed ? subdivided ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2521
- className: panel_module_css_default.projectNest,
2522
- children: ["project-dsh", "project-agents"].map((source) => {
2523
- const list = proj.skills.filter((skill) => skill.source === source);
2524
- if (list.length === 0) return null;
2525
- const srcKey = projKey + ":" + source;
2526
- const srcCollapsed = collapsedGroups.has(srcKey);
2527
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2528
- className: panel_module_css_default.projectNest,
2529
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2530
- className: panel_module_css_default.groupHead,
2531
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2650
+ }), !topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
2651
+ const projKey = "project:" + key;
2652
+ const projCollapsed = collapsedGroups.has(projKey);
2653
+ const subdivided = hub.subdividedProjects.has(key);
2654
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2655
+ className: panel_module_css_default.projectNest,
2656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2657
+ className: panel_module_css_default.groupHead,
2658
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2659
+ type: "button",
2660
+ className: panel_module_css_default.disclosure,
2661
+ "aria-expanded": !projCollapsed,
2662
+ onClick: () => {
2663
+ toggleGroupCollapse(projKey);
2664
+ },
2665
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (projCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2666
+ className: panel_module_css_default.groupTitle,
2667
+ children: [
2668
+ proj.title,
2669
+ " · ",
2670
+ proj.skills.length,
2671
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2672
+ members: proj.skills.map((s) => s.name),
2673
+ hub
2674
+ })
2675
+ ]
2676
+ })]
2677
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2678
+ className: panel_module_css_default.groupOps,
2679
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2532
2680
  type: "button",
2533
- className: panel_module_css_default.disclosure,
2534
- "aria-expanded": !srcCollapsed,
2535
- onClick: () => {
2536
- toggleGroupCollapse(srcKey);
2681
+ className: panel_module_css_default.opBtn,
2682
+ onClick: (event) => {
2683
+ event.stopPropagation();
2684
+ hub.toggleSubdivide(key);
2537
2685
  },
2538
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (srcCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2539
- className: panel_module_css_default.groupTitle,
2540
- children: [
2541
- tt("badge.source." + source),
2542
- " · ",
2543
- list.length
2544
- ]
2545
- })]
2686
+ children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2546
2687
  })
2547
- }), !srcCollapsed ? list.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2548
- skill,
2549
- hub
2550
- }, skill.name)) : null]
2551
- }, srcKey);
2552
- })
2553
- }) : proj.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2554
- skill,
2555
- hub
2556
- }, skill.name)) : null]
2557
- }, projKey);
2558
- }) : null]
2559
- });
2560
- }
2561
- function SourcesView(props) {
2562
- const { hub } = props;
2563
- const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, toggleGroup, enableDisabled } = hub;
2564
- 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, {
2565
- skill,
2566
- hub
2567
- }, skill.name)) });
2568
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2569
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTree, { hub }),
2570
- groupsState !== null && groupsState.collections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2571
- className: panel_module_css_default.empty,
2572
- children: tt("groups.noCollections")
2573
- }) : null,
2574
- groupsState?.collections.map((collection) => {
2688
+ })]
2689
+ }), !projCollapsed ? subdivided ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2690
+ className: panel_module_css_default.projectNest,
2691
+ children: ["project-dsh", "project-agents"].map((source) => {
2692
+ const list = proj.skills.filter((skill) => skill.source === source);
2693
+ if (list.length === 0) return null;
2694
+ const srcKey = projKey + ":" + source;
2695
+ const srcCollapsed = collapsedGroups.has(srcKey);
2696
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2697
+ className: panel_module_css_default.projectNest,
2698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2699
+ className: panel_module_css_default.groupHead,
2700
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2701
+ type: "button",
2702
+ className: panel_module_css_default.disclosure,
2703
+ "aria-expanded": !srcCollapsed,
2704
+ onClick: () => {
2705
+ toggleGroupCollapse(srcKey);
2706
+ },
2707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (srcCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2708
+ className: panel_module_css_default.groupTitle,
2709
+ children: [
2710
+ tt("badge.source." + source),
2711
+ " · ",
2712
+ list.length
2713
+ ]
2714
+ })]
2715
+ })
2716
+ }), !srcCollapsed ? list.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2717
+ skill,
2718
+ hub
2719
+ }, skill.name)) : null]
2720
+ }, srcKey);
2721
+ })
2722
+ }) : proj.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2723
+ skill,
2724
+ hub
2725
+ }, skill.name)) : null]
2726
+ }, projKey);
2727
+ }) : null]
2728
+ }, "project");
2729
+ }
2730
+ if (topKey.startsWith("col:")) {
2731
+ const colName = topKey.slice(4);
2732
+ const collection = collections.find((c) => c.name === colName);
2733
+ if (collection === void 0) return null;
2575
2734
  const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name));
2576
2735
  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));
2577
2736
  const collapsed = collapsedGroups.has("col:" + collection.name);
2578
2737
  const view = groupSwitchView(collection.skillNames, viewNames);
2579
2738
  const check = sourceCheck[collection.name];
2580
2739
  const hasWritable = collection.skillNames.some((name) => actionNames.has(name));
2740
+ const isDragging = topDragKey === topKey;
2741
+ const isOver = topOverKey === topKey && topDragKey !== topKey;
2581
2742
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2582
- className: panel_module_css_default.section,
2743
+ className: panel_module_css_default.section + (isDragging ? " " + panel_module_css_default.dragging : "") + (isOver ? " " + panel_module_css_default.dragOver : ""),
2744
+ draggable: true,
2745
+ onDragStart: (e) => {
2746
+ setTopDragKey(topKey);
2747
+ e.dataTransfer.effectAllowed = "move";
2748
+ e.dataTransfer.setData("text/plain", topKey);
2749
+ },
2750
+ onDragOver: (e) => {
2751
+ e.preventDefault();
2752
+ if (topOverKey !== topKey) setTopOverKey(topKey);
2753
+ },
2754
+ onDragLeave: () => {
2755
+ if (topOverKey === topKey) setTopOverKey(null);
2756
+ },
2757
+ onDrop: (e) => {
2758
+ e.preventDefault();
2759
+ handleTopDrop(topKey);
2760
+ setTopOverKey(null);
2761
+ },
2762
+ onDragEnd: () => {
2763
+ setTopDragKey(null);
2764
+ setTopOverKey(null);
2765
+ },
2583
2766
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2584
2767
  className: panel_module_css_default.groupHead,
2585
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2586
- type: "button",
2587
- className: panel_module_css_default.disclosure,
2588
- "aria-expanded": !collapsed,
2589
- onClick: () => {
2590
- toggleGroupCollapse("col:" + collection.name);
2591
- },
2592
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2593
- className: panel_module_css_default.groupTitle,
2768
+ children: [
2769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2770
+ className: panel_module_css_default.dragHandle,
2771
+ "aria-hidden": true,
2772
+ title: "拖拽调整顺序",
2773
+ children: "⋮⋮"
2774
+ }),
2775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2776
+ type: "button",
2777
+ className: panel_module_css_default.disclosure,
2778
+ "aria-expanded": !collapsed,
2779
+ onClick: () => {
2780
+ toggleGroupCollapse("col:" + collection.name);
2781
+ },
2782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2783
+ className: panel_module_css_default.groupTitle,
2784
+ children: [
2785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2786
+ className: panel_module_css_default.sourceLink,
2787
+ href: "https://github.com/" + collection.name,
2788
+ target: "_blank",
2789
+ rel: "noreferrer",
2790
+ onClick: (event) => {
2791
+ event.stopPropagation();
2792
+ },
2793
+ children: collection.name
2794
+ }),
2795
+ " · " + collection.skillNames.length,
2796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2797
+ members: collection.skillNames,
2798
+ hub
2799
+ })
2800
+ ]
2801
+ })]
2802
+ }),
2803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2804
+ className: panel_module_css_default.groupOps,
2594
2805
  children: [
2595
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2596
- className: panel_module_css_default.sourceLink,
2597
- href: "https://github.com/" + collection.name,
2598
- target: "_blank",
2599
- rel: "noreferrer",
2806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceStatusBadge, {
2807
+ check,
2808
+ checking: checkingSource === collection.name,
2809
+ onCheck: () => {
2810
+ checkSources(collection.name);
2811
+ }
2812
+ }),
2813
+ check !== void 0 && check.changed && check.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2814
+ type: "button",
2815
+ className: panel_module_css_default.opBtn,
2816
+ disabled: syncingSource !== null,
2817
+ onClick: (event) => {
2818
+ event.stopPropagation();
2819
+ requestSync(collection.name, check.updated);
2820
+ },
2821
+ children: syncingSource === collection.name ? tt("source.syncing") : tt("source.sync")
2822
+ }) : null,
2823
+ check !== void 0 && check.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2824
+ type: "button",
2825
+ className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2826
+ onClick: (event) => {
2827
+ event.stopPropagation();
2828
+ requestDelete(collection.name, check.deleted);
2829
+ },
2830
+ children: tt("source.followDelete")
2831
+ }) : null,
2832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2833
+ type: "button",
2834
+ role: "switch",
2835
+ "aria-checked": view.state !== "off",
2836
+ "aria-label": collection.name,
2837
+ className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
2838
+ disabled: batchBusy || collection.skillNames.length === 0 || view.state !== "off" && !hasWritable,
2839
+ title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
2600
2840
  onClick: (event) => {
2601
2841
  event.stopPropagation();
2842
+ toggleGroup("col:" + collection.name, collection.name, view.state);
2602
2843
  },
2603
- children: collection.name
2844
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2604
2845
  }),
2605
- " · " + collection.skillNames.length,
2606
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2607
- members: collection.skillNames,
2608
- hub
2609
- })
2846
+ hub.editMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2847
+ type: "button",
2848
+ className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2849
+ title: tt("source.deleteGroupHint", { count: collection.skillNames.length }),
2850
+ onClick: (event) => {
2851
+ event.stopPropagation();
2852
+ requestDeleteGroup(collection.name, collection.skillNames);
2853
+ },
2854
+ children: tt("source.deleteGroup")
2855
+ }) : null
2610
2856
  ]
2611
- })]
2612
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2613
- className: panel_module_css_default.groupOps,
2614
- children: [
2615
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceStatusBadge, {
2616
- check,
2617
- checking: checkingSource === collection.name,
2618
- onCheck: () => {
2619
- checkSources(collection.name);
2620
- }
2621
- }),
2622
- check !== void 0 && check.changed && check.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2623
- type: "button",
2624
- className: panel_module_css_default.opBtn,
2625
- disabled: syncingSource !== null,
2626
- onClick: (event) => {
2627
- event.stopPropagation();
2628
- requestSync(collection.name, check.updated);
2629
- },
2630
- children: syncingSource === collection.name ? tt("source.syncing") : tt("source.sync")
2631
- }) : null,
2632
- check !== void 0 && check.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2633
- type: "button",
2634
- className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2635
- onClick: (event) => {
2636
- event.stopPropagation();
2637
- requestDelete(collection.name, check.deleted);
2638
- },
2639
- children: tt("source.followDelete")
2640
- }) : null,
2641
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2642
- type: "button",
2643
- role: "switch",
2644
- "aria-checked": view.state !== "off",
2645
- "aria-label": collection.name,
2646
- className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
2647
- disabled: batchBusy || collection.skillNames.length === 0 || view.state !== "off" && !hasWritable,
2648
- title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
2649
- onClick: (event) => {
2650
- event.stopPropagation();
2651
- toggleGroup("col:" + collection.name, collection.name, view.state);
2652
- },
2653
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2654
- })
2655
- ]
2656
- })]
2857
+ })
2858
+ ]
2657
2859
  }), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2658
2860
  skill,
2659
2861
  hub
@@ -2665,48 +2867,121 @@ window.__ModuleLoader__.load({
2665
2867
  }
2666
2868
  }, record.name))] }) : null]
2667
2869
  }, "col:" + collection.name);
2668
- }),
2669
- (() => {
2870
+ }
2871
+ if (topKey === "uncategorized-source" && hasPersonal) {
2670
2872
  const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
2671
- if (uncategorized.length === 0) return null;
2873
+ 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");
2874
+ const allPersonalNames = [...uncategorized.map((s) => s.name), ...personalDisabled.map((r) => r.name)];
2875
+ if (allPersonalNames.length === 0) return null;
2672
2876
  const collapsed = collapsedGroups.has("uncategorized-source");
2877
+ const isDragging = topDragKey === "uncategorized-source";
2878
+ const isOver = topOverKey === "uncategorized-source" && topDragKey !== "uncategorized-source";
2673
2879
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2674
- className: panel_module_css_default.section,
2675
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2880
+ className: panel_module_css_default.section + (isDragging ? " " + panel_module_css_default.dragging : "") + (isOver ? " " + panel_module_css_default.dragOver : ""),
2881
+ draggable: true,
2882
+ onDragStart: (e) => {
2883
+ setTopDragKey("uncategorized-source");
2884
+ e.dataTransfer.effectAllowed = "move";
2885
+ e.dataTransfer.setData("text/plain", "uncategorized-source");
2886
+ },
2887
+ onDragOver: (e) => {
2888
+ e.preventDefault();
2889
+ if (topOverKey !== "uncategorized-source") setTopOverKey("uncategorized-source");
2890
+ },
2891
+ onDragLeave: () => {
2892
+ if (topOverKey === "uncategorized-source") setTopOverKey(null);
2893
+ },
2894
+ onDrop: (e) => {
2895
+ e.preventDefault();
2896
+ handleTopDrop("uncategorized-source");
2897
+ setTopOverKey(null);
2898
+ },
2899
+ onDragEnd: () => {
2900
+ setTopDragKey(null);
2901
+ setTopOverKey(null);
2902
+ },
2903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2676
2904
  className: panel_module_css_default.groupHead,
2677
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2678
- type: "button",
2679
- className: panel_module_css_default.disclosure,
2680
- "aria-expanded": !collapsed,
2681
- onClick: () => {
2682
- toggleGroupCollapse("uncategorized-source");
2683
- },
2684
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2685
- className: panel_module_css_default.groupTitle,
2686
- children: [
2687
- tt("groups.personal"),
2688
- " · ",
2689
- uncategorized.length,
2690
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2691
- members: uncategorized.map((skill) => skill.name),
2692
- hub
2693
- })
2694
- ]
2695
- })]
2696
- })
2697
- }), !collapsed ? uncategorized.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2905
+ children: [
2906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2907
+ className: panel_module_css_default.dragHandle,
2908
+ "aria-hidden": true,
2909
+ title: "拖拽调整顺序",
2910
+ children: "⋮⋮"
2911
+ }),
2912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2913
+ type: "button",
2914
+ className: panel_module_css_default.disclosure,
2915
+ "aria-expanded": !collapsed,
2916
+ onClick: () => {
2917
+ toggleGroupCollapse("uncategorized-source");
2918
+ },
2919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2920
+ className: panel_module_css_default.groupTitle,
2921
+ children: [
2922
+ tt("groups.personal"),
2923
+ " · ",
2924
+ allPersonalNames.length,
2925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2926
+ members: allPersonalNames,
2927
+ hub
2928
+ })
2929
+ ]
2930
+ })]
2931
+ }),
2932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2933
+ className: panel_module_css_default.groupOps,
2934
+ children: hub.editMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2935
+ type: "button",
2936
+ className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2937
+ title: tt("source.deleteGroupHint", { count: allPersonalNames.length }),
2938
+ onClick: (event) => {
2939
+ event.stopPropagation();
2940
+ requestDeleteGroup(tt("groups.personal"), allPersonalNames);
2941
+ },
2942
+ children: tt("source.deleteGroup")
2943
+ }) : null
2944
+ })
2945
+ ]
2946
+ }), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [uncategorized.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2698
2947
  skill,
2699
2948
  hub
2700
- }, skill.name)) : null]
2701
- });
2702
- })()
2703
- ] });
2949
+ }, skill.name)), personalDisabled.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
2950
+ record,
2951
+ busy: busyNames.has(record.name),
2952
+ onEnable: () => {
2953
+ enableDisabled(record);
2954
+ }
2955
+ }, record.name))] }) : null]
2956
+ }, "uncategorized-source");
2957
+ }
2958
+ return null;
2959
+ })] });
2704
2960
  }
2705
2961
  //#endregion
2706
2962
  //#region src/client/panel/ScenesView.tsx
2963
+ /**
2964
+ * Scenes tab: user tag groups (one card per scene with the tri-state switch
2965
+ * and edit entry) plus the new-scene form. Scenes are the user's own
2966
+ * enable/disable units (e.g. a Godot scene vs a Java scene); upstream repos
2967
+ * are managed in the sources tab.
2968
+ */
2707
2969
  function ScenesView(props) {
2708
2970
  const { hub } = props;
2709
2971
  const { catalog, groupsState, sorted, normalized, collapsedGroups, viewNames, actionNames, batchBusy, busyNames, newTagName, setNewTagName, tagBusy, createTag, toggleGroupCollapse, toggleGroup, setEditingTag, setEditName, setMembersDraft, setEditSearch, enableDisabled } = hub;
2972
+ const [dragId, setDragId] = (0, react.useState)(null);
2973
+ const [overId, setOverId] = (0, react.useState)(null);
2974
+ const handleDrop = (targetId) => {
2975
+ if (dragId === null || dragId === targetId || groupsState === null) return;
2976
+ const ids = groupsState.tags.map((t) => t.id);
2977
+ const from = ids.indexOf(dragId);
2978
+ const to = ids.indexOf(targetId);
2979
+ if (from === -1 || to === -1) return;
2980
+ const next = [...ids];
2981
+ const [moved] = next.splice(from, 1);
2982
+ next.splice(to, 0, moved);
2983
+ hub.reorderTags(next);
2984
+ };
2710
2985
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2711
2986
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
2712
2987
  className: panel_module_css_default.form,
@@ -2740,56 +3015,89 @@ window.__ModuleLoader__.load({
2740
3015
  const collapsed = collapsedGroups.has("tag:" + tag.id);
2741
3016
  const view = groupSwitchView(tag.skillNames, viewNames);
2742
3017
  const hasWritable = tag.skillNames.some((name) => actionNames.has(name));
3018
+ const isDragging = dragId === tag.id;
3019
+ const isOver = overId === tag.id && dragId !== tag.id;
2743
3020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2744
- className: panel_module_css_default.section,
3021
+ className: panel_module_css_default.section + (isDragging ? " " + panel_module_css_default.dragging : "") + (isOver ? " " + panel_module_css_default.dragOver : ""),
3022
+ draggable: true,
3023
+ onDragStart: (e) => {
3024
+ setDragId(tag.id);
3025
+ e.dataTransfer.effectAllowed = "move";
3026
+ e.dataTransfer.setData("text/plain", tag.id);
3027
+ },
3028
+ onDragOver: (e) => {
3029
+ e.preventDefault();
3030
+ if (overId !== tag.id) setOverId(tag.id);
3031
+ },
3032
+ onDragLeave: () => {
3033
+ if (overId === tag.id) setOverId(null);
3034
+ },
3035
+ onDrop: (e) => {
3036
+ e.preventDefault();
3037
+ handleDrop(tag.id);
3038
+ setOverId(null);
3039
+ },
3040
+ onDragEnd: () => {
3041
+ setDragId(null);
3042
+ setOverId(null);
3043
+ },
2745
3044
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2746
3045
  className: panel_module_css_default.groupHead,
2747
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2748
- type: "button",
2749
- className: panel_module_css_default.disclosure,
2750
- "aria-expanded": !collapsed,
2751
- onClick: () => {
2752
- toggleGroupCollapse("tag:" + tag.id);
2753
- },
2754
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2755
- className: panel_module_css_default.groupTitle,
2756
- children: [
2757
- tag.name,
2758
- " · ",
2759
- tag.skillNames.length,
2760
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2761
- members: tag.skillNames,
2762
- hub
2763
- })
2764
- ]
2765
- })]
2766
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2767
- className: panel_module_css_default.groupOps,
2768
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2769
- type: "button",
2770
- role: "switch",
2771
- "aria-checked": view.state !== "off",
2772
- "aria-label": tag.name,
2773
- className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
2774
- disabled: batchBusy || tag.skillNames.length === 0 || view.state !== "off" && !hasWritable,
2775
- title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
2776
- onClick: (event) => {
2777
- event.stopPropagation();
2778
- toggleGroup("tag:" + tag.id, tag.name, view.state);
2779
- },
2780
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2781
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3046
+ children: [
3047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3048
+ className: panel_module_css_default.dragHandle,
3049
+ "aria-hidden": true,
3050
+ title: "拖拽调整顺序",
3051
+ children: "⋮⋮"
3052
+ }),
3053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2782
3054
  type: "button",
2783
- className: panel_module_css_default.opBtn,
3055
+ className: panel_module_css_default.disclosure,
3056
+ "aria-expanded": !collapsed,
2784
3057
  onClick: () => {
2785
- setEditingTag(tag);
2786
- setEditName(tag.name);
2787
- setMembersDraft(new Set(tag.skillNames));
2788
- setEditSearch("");
3058
+ toggleGroupCollapse("tag:" + tag.id);
2789
3059
  },
2790
- children: tt("groups.edit")
2791
- })]
2792
- })]
3060
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3061
+ className: panel_module_css_default.groupTitle,
3062
+ children: [
3063
+ tag.name,
3064
+ " · ",
3065
+ tag.skillNames.length,
3066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
3067
+ members: tag.skillNames,
3068
+ hub
3069
+ })
3070
+ ]
3071
+ })]
3072
+ }),
3073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3074
+ className: panel_module_css_default.groupOps,
3075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3076
+ type: "button",
3077
+ role: "switch",
3078
+ "aria-checked": view.state !== "off",
3079
+ "aria-label": tag.name,
3080
+ className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
3081
+ disabled: batchBusy || tag.skillNames.length === 0 || view.state !== "off" && !hasWritable,
3082
+ title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
3083
+ onClick: (event) => {
3084
+ event.stopPropagation();
3085
+ toggleGroup("tag:" + tag.id, tag.name, view.state);
3086
+ },
3087
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
3088
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3089
+ type: "button",
3090
+ className: panel_module_css_default.opBtn,
3091
+ onClick: () => {
3092
+ setEditingTag(tag);
3093
+ setEditName(tag.name);
3094
+ setMembersDraft(new Set(tag.skillNames));
3095
+ setEditSearch("");
3096
+ },
3097
+ children: tt("groups.edit")
3098
+ })]
3099
+ })
3100
+ ]
2793
3101
  }), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2794
3102
  skill,
2795
3103
  hub
@@ -3042,17 +3350,66 @@ window.__ModuleLoader__.load({
3042
3350
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
3043
3351
  return (bytes / 1024 / 1024).toFixed(1) + " MB";
3044
3352
  }
3353
+ function formatSpeed(bps) {
3354
+ if (bps === void 0 || bps <= 0) return "";
3355
+ if (bps < 1024) return bps + " B/s";
3356
+ if (bps < 1024 * 1024) return (bps / 1024).toFixed(1) + " KB/s";
3357
+ return (bps / 1024 / 1024).toFixed(1) + " MB/s";
3358
+ }
3045
3359
  function MarketView(props) {
3046
3360
  const { hub } = props;
3047
- const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
3361
+ const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, checkingSource, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, cancelImport, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
3048
3362
  /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
3049
3363
  const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
3050
- /** 一个已添加来源的完整行:状态徽章 + 操作按钮。 */
3364
+ const [repoSearch, setRepoSearch] = (0, react.useState)("");
3365
+ const [repoFilter, setRepoFilter] = (0, react.useState)("all");
3366
+ const [visibleCount, setVisibleCount] = (0, react.useState)(50);
3367
+ (0, react.useEffect)(() => {
3368
+ setVisibleCount(50);
3369
+ }, [
3370
+ repoDiscoverState,
3371
+ repoSearch,
3372
+ repoFilter
3373
+ ]);
3374
+ /** 一个已添加来源的完整行:状态徽章 + 操作按钮。B 方案:扫描主按钮 + 智能更新按钮(检查/更新二合一)+ 溢出移除 */
3051
3375
  const sourceRow = (record) => {
3052
3376
  const releaseCheck = marketCheck[record.repo];
3053
3377
  const skillCheck = sourceCheck[record.repo];
3054
3378
  const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
3055
3379
  const scanning = scanningRepo === record.repo;
3380
+ const syncing = syncingMarket === record.repo;
3381
+ const checking = checkingSource === record.repo;
3382
+ const hasSkillUpdate = skillCheck?.changed === true && skillCheck.updated.length > 0;
3383
+ const hasReleaseUpdate = releaseCheck?.updateAvailable === true;
3384
+ const hasUpdate = hasSkillUpdate || hasReleaseUpdate;
3385
+ const isChecked = releaseCheck !== void 0 || skillCheck !== void 0;
3386
+ let updateLabel;
3387
+ let updateDisabled = false;
3388
+ let updateDanger = false;
3389
+ let updateAction = () => {};
3390
+ if (syncing) {
3391
+ updateLabel = tt("market.syncing");
3392
+ updateDisabled = true;
3393
+ } else if (checking) {
3394
+ updateLabel = tt("market.checking");
3395
+ updateDisabled = true;
3396
+ } else if (!isChecked) {
3397
+ updateLabel = tt("market.checkUpdate");
3398
+ updateAction = () => {
3399
+ checkSources(record.repo);
3400
+ checkMarket();
3401
+ };
3402
+ } else if (hasUpdate) {
3403
+ const count = hasSkillUpdate ? skillCheck.updated.length : 1;
3404
+ updateLabel = count > 1 || hasSkillUpdate ? tt("market.updateCount", { count }) : tt("market.update");
3405
+ updateDanger = true;
3406
+ updateAction = () => {
3407
+ syncMarketSource(record.repo);
3408
+ };
3409
+ } else {
3410
+ updateLabel = tt("market.upToDate");
3411
+ updateDisabled = true;
3412
+ }
3056
3413
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3057
3414
  className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
3058
3415
  children: [
@@ -3076,7 +3433,7 @@ window.__ModuleLoader__.load({
3076
3433
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
3077
3434
  children: tt("market.installed", { count: installedCount })
3078
3435
  }) : null,
3079
- skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3436
+ hasSkillUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3080
3437
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
3081
3438
  children: tt("market.updatable", { count: skillCheck.updated.length })
3082
3439
  }) : null,
@@ -3084,7 +3441,7 @@ window.__ModuleLoader__.load({
3084
3441
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
3085
3442
  children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
3086
3443
  }) : null,
3087
- releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3444
+ hasReleaseUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3088
3445
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
3089
3446
  children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
3090
3447
  }) : null
@@ -3093,40 +3450,39 @@ window.__ModuleLoader__.load({
3093
3450
  }),
3094
3451
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3095
3452
  type: "button",
3096
- className: panel_module_css_default.opBtn,
3097
- disabled: scanning,
3098
- onClick: () => {
3099
- checkMarket();
3100
- },
3101
- children: tt("market.check")
3102
- }),
3103
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3104
- type: "button",
3105
- className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
3106
- disabled: syncingMarket === record.repo,
3107
- onClick: () => {
3108
- syncMarketSource(record.repo);
3453
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3454
+ style: {
3455
+ padding: "4px 10px",
3456
+ fontSize: 12
3109
3457
  },
3110
- children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
3111
- }),
3112
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3113
- type: "button",
3114
- className: panel_module_css_default.opBtn,
3115
3458
  disabled: scanning,
3116
3459
  onClick: () => {
3117
3460
  scanRepo(record.repo);
3118
3461
  },
3119
3462
  children: scanning ? tt("market.scanning") : tt("market.scan")
3120
3463
  }),
3464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3465
+ type: "button",
3466
+ className: panel_module_css_default.opBtn + (updateDanger ? " " + panel_module_css_default.opDanger : ""),
3467
+ disabled: updateDisabled,
3468
+ title: hasUpdate ? "将同步到上游最新版并提示可更新的本地技能" : void 0,
3469
+ onClick: updateAction,
3470
+ children: updateLabel
3471
+ }),
3121
3472
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3122
3473
  type: "button",
3123
3474
  className: panel_module_css_default.opBtn,
3124
3475
  disabled: tagBusy,
3125
3476
  title: tt("market.removeHint"),
3477
+ "aria-label": tt("market.deleteSource"),
3126
3478
  onClick: () => {
3127
3479
  removeMarketSource(record.repo);
3128
3480
  },
3129
- children: tt("market.deleteSource")
3481
+ style: {
3482
+ padding: "4px 8px",
3483
+ minWidth: 28
3484
+ },
3485
+ children: "×"
3130
3486
  })
3131
3487
  ]
3132
3488
  });
@@ -3251,109 +3607,379 @@ window.__ModuleLoader__.load({
3251
3607
  marketState.status !== "loading" ? rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: row.element }, row.key)) : null
3252
3608
  ]
3253
3609
  }),
3254
- repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3255
- className: panel_module_css_default.empty,
3256
- children: tt("market.scanning")
3257
- }) : null,
3258
- repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3259
- className: panel_module_css_default.errorBanner,
3260
- children: repoDiscoverState.message
3261
- }) : null,
3262
- repoDiscoverState.status === "ready" ? (() => {
3263
- const entries = repoDiscoverState.data.entries;
3264
- const selected = entries.filter((entry) => repoSelected.has(entry.path) && !entry.existing);
3265
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3266
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
3267
- children: tt("repo.ready", { count: entries.length })
3268
- }), entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3269
- className: panel_module_css_default.empty,
3270
- children: tt("repo.empty")
3271
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3272
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3273
- className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsBottom,
3274
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3275
- type: "button",
3276
- className: panel_module_css_default.button,
3277
- onClick: () => {
3278
- setRepoSelected(new Set(entries.filter((entry) => !entry.existing).map((entry) => entry.path)));
3279
- },
3280
- children: tt("repo.selectAll")
3281
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3282
- type: "button",
3283
- className: panel_module_css_default.button,
3284
- onClick: () => {
3285
- setRepoSelected(/* @__PURE__ */ new Set());
3286
- },
3287
- children: tt("repo.clearAll")
3288
- })]
3289
- }),
3290
- ["skills", "design-templates"].map((root) => {
3291
- const rootEntries = entries.filter((entry) => entry.root === root);
3292
- if (rootEntries.length === 0) return null;
3293
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3294
- className: panel_module_css_default.section,
3295
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3296
- className: panel_module_css_default.sectionTitle,
3297
- children: root === "skills" ? tt("repo.root.skills") : tt("repo.root.designTemplates")
3298
- }), rootEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3299
- className: panel_module_css_default.row + (entry.existing ? " " + panel_module_css_default.rowMuted : ""),
3610
+ repoDiscoverState.status !== "idle" || repoResult !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3611
+ className: panel_module_css_default.section,
3612
+ style: {
3613
+ borderLeft: "3px solid var(--hub-model, #2f81f7)",
3614
+ background: "rgba(47,129,247,0.04)"
3615
+ },
3616
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3617
+ className: panel_module_css_default.sectionTitle + " " + panel_module_css_default.sectionHeadRow,
3618
+ style: { margin: "10px 14px 6px" },
3619
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3620
+ className: panel_module_css_default.sectionTitleFill,
3621
+ style: {
3622
+ display: "flex",
3623
+ alignItems: "center",
3624
+ gap: 6
3625
+ },
3626
+ children: [
3627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
3628
+ width: 7,
3629
+ height: 7,
3630
+ borderRadius: "50%",
3631
+ background: "var(--hub-model, #2f81f7)",
3632
+ display: "inline-block"
3633
+ } }),
3634
+ "扫描结果",
3635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3636
+ style: {
3637
+ fontWeight: 400,
3638
+ opacity: .75
3639
+ },
3300
3640
  children: [
3301
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3302
- type: "checkbox",
3303
- checked: repoSelected.has(entry.path),
3304
- disabled: entry.existing,
3305
- onChange: (event) => {
3306
- toggleRepoSelected(entry.path, event.target.checked);
3307
- }
3308
- }),
3309
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3310
- className: panel_module_css_default.rowMain,
3311
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3312
- className: panel_module_css_default.rowName,
3313
- children: entry.name
3314
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3315
- className: panel_module_css_default.rowDesc,
3316
- children: [
3317
- entry.dir,
3318
- " · ",
3319
- tt("repo.files", {
3320
- count: entry.fileCount,
3321
- size: formatBytes(entry.totalBytes)
3322
- })
3323
- ]
3324
- })]
3325
- }),
3326
- entry.existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3327
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
3328
- children: tt("repo.existing")
3329
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3330
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
3331
- children: entry.origin
3332
- })
3641
+ "— ",
3642
+ repoDiscoverState.status === "ready" ? repoDiscoverState.data.repo : repoDiscoverState.status === "error" ? scanningRepo ?? "" : scanningRepo ?? "",
3643
+ repoDiscoverState.status === "ready" && repoDiscoverState.data.ref !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3644
+ style: {
3645
+ opacity: .5,
3646
+ marginLeft: 6
3647
+ },
3648
+ children: ["ref ", repoDiscoverState.data.ref]
3649
+ }) : null
3333
3650
  ]
3334
- }, entry.path))]
3335
- }, root);
3336
- }),
3337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3651
+ })
3652
+ ]
3653
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3338
3654
  type: "button",
3339
- className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3340
- disabled: selected.length === 0 || repoImporting,
3655
+ className: panel_module_css_default.opBtn,
3341
3656
  onClick: () => {
3342
- importRepo();
3657
+ hub.clearScan();
3343
3658
  },
3344
- children: repoImporting ? tt("repo.importing") : tt("repo.import", { count: selected.length })
3345
- })
3346
- ] })] });
3347
- })() : null,
3348
- repoResult !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3349
- className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
3350
- children: [
3351
- tt("repo.imported", { count: repoResult.imported.length }),
3352
- " · ",
3353
- tt("repo.skippedExisting", { count: repoResult.skipped.length }),
3354
- " · ",
3355
- tt("repo.failed", { count: repoResult.failed.length })
3356
- ]
3659
+ children: "关闭"
3660
+ })]
3661
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3662
+ style: { padding: "0 12px 12px" },
3663
+ children: [
3664
+ repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3665
+ className: panel_module_css_default.empty,
3666
+ style: { padding: "12px 0" },
3667
+ children: tt("market.scanning")
3668
+ }) : null,
3669
+ repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3670
+ className: panel_module_css_default.errorBanner,
3671
+ children: repoDiscoverState.message
3672
+ }) : null,
3673
+ repoDiscoverState.status === "ready" ? (() => {
3674
+ const entries = repoDiscoverState.data.entries;
3675
+ const installedNames = /* @__PURE__ */ new Set([
3676
+ ...(hub.catalog?.skills ?? []).map((s) => s.name),
3677
+ ...(hub.catalog?.disabled ?? []).map((d) => d.name),
3678
+ ...(repoResult?.imported ?? []).map((r) => r.name),
3679
+ ...(repoResult?.skipped ?? []).map((r) => r.name)
3680
+ ]);
3681
+ const isExisting = (entry) => entry.existing || installedNames.has(entry.name);
3682
+ const selected = entries.filter((entry) => repoSelected.has(entry.path) && !isExisting(entry));
3683
+ const selectedBytes = selected.reduce((s, e) => s + e.totalBytes, 0);
3684
+ const filtered = entries.filter((entry) => {
3685
+ if (repoFilter !== "all" && entry.root !== repoFilter) return false;
3686
+ if (repoSearch.trim() !== "" && !entry.name.toLowerCase().includes(repoSearch.trim().toLowerCase())) return false;
3687
+ return true;
3688
+ }).sort((a, b) => a.name.localeCompare(b.name));
3689
+ const paged = filtered.slice(0, visibleCount);
3690
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3691
+ className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
3692
+ children: [
3693
+ tt("repo.ready", { count: entries.length }),
3694
+ " · ",
3695
+ repoDiscoverState.data.ref !== null ? `ref ${repoDiscoverState.data.ref}` : ""
3696
+ ]
3697
+ }), entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3698
+ className: panel_module_css_default.empty,
3699
+ children: tt("repo.empty")
3700
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3701
+ (() => {
3702
+ const order = /* @__PURE__ */ new Map([["skills", 0], ["design-templates", 1]]);
3703
+ const roots = [...new Set(entries.map((e) => e.root))].sort((a, b) => {
3704
+ const ao = order.get(a) ?? 99;
3705
+ const bo = order.get(b) ?? 99;
3706
+ return ao !== bo ? ao - bo : a.localeCompare(b);
3707
+ });
3708
+ const counts = /* @__PURE__ */ new Map();
3709
+ for (const e of entries) counts.set(e.root, (counts.get(e.root) ?? 0) + 1);
3710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3711
+ style: {
3712
+ display: "flex",
3713
+ gap: 8,
3714
+ marginBottom: 8,
3715
+ flexWrap: "wrap"
3716
+ },
3717
+ children: [
3718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3719
+ className: panel_module_css_default.input,
3720
+ style: {
3721
+ flex: 1,
3722
+ minWidth: 140
3723
+ },
3724
+ value: repoSearch,
3725
+ onChange: (e) => setRepoSearch(e.target.value),
3726
+ placeholder: "搜索技能名…"
3727
+ }),
3728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3729
+ type: "button",
3730
+ className: panel_module_css_default.button + (repoFilter === "all" ? " " + panel_module_css_default.primary : ""),
3731
+ style: {
3732
+ padding: "6px 10px",
3733
+ fontSize: 12
3734
+ },
3735
+ onClick: () => setRepoFilter("all"),
3736
+ children: ["全部 ", entries.length]
3737
+ }),
3738
+ roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3739
+ type: "button",
3740
+ className: panel_module_css_default.button + (repoFilter === root ? " " + panel_module_css_default.primary : ""),
3741
+ style: {
3742
+ padding: "6px 10px",
3743
+ fontSize: 12
3744
+ },
3745
+ onClick: () => setRepoFilter(root),
3746
+ children: [
3747
+ root,
3748
+ " ",
3749
+ counts.get(root) ?? 0
3750
+ ]
3751
+ }, root))
3752
+ ]
3753
+ });
3754
+ })(),
3755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3756
+ className: panel_module_css_default.hintLine,
3757
+ style: {
3758
+ display: "flex",
3759
+ justifyContent: "space-between",
3760
+ flexWrap: "wrap",
3761
+ gap: 8
3762
+ },
3763
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3764
+ "已选 ",
3765
+ selected.length,
3766
+ "/",
3767
+ entries.length,
3768
+ " · ",
3769
+ formatBytes(selectedBytes),
3770
+ " · 显示 ",
3771
+ paged.length,
3772
+ "/",
3773
+ filtered.length,
3774
+ "(过滤后)"
3775
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3776
+ style: {
3777
+ display: "flex",
3778
+ gap: 6
3779
+ },
3780
+ children: [
3781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3782
+ type: "button",
3783
+ className: panel_module_css_default.button,
3784
+ style: {
3785
+ padding: "4px 8px",
3786
+ fontSize: 11
3787
+ },
3788
+ onClick: () => {
3789
+ setRepoSelected(new Set(filtered.filter((e) => !isExisting(e)).slice(0, visibleCount).map((e) => e.path)));
3790
+ },
3791
+ children: "全选当前显示"
3792
+ }),
3793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3794
+ type: "button",
3795
+ className: panel_module_css_default.button,
3796
+ style: {
3797
+ padding: "4px 8px",
3798
+ fontSize: 11
3799
+ },
3800
+ onClick: () => {
3801
+ setRepoSelected(new Set(entries.filter((e) => !isExisting(e)).map((e) => e.path)));
3802
+ },
3803
+ children: ["全选全部 ", entries.filter((e) => !isExisting(e)).length]
3804
+ }),
3805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3806
+ type: "button",
3807
+ className: panel_module_css_default.button,
3808
+ style: {
3809
+ padding: "4px 8px",
3810
+ fontSize: 11
3811
+ },
3812
+ onClick: () => {
3813
+ setRepoSelected(/* @__PURE__ */ new Set());
3814
+ },
3815
+ children: tt("repo.clearAll")
3816
+ })
3817
+ ]
3818
+ })]
3819
+ }),
3820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3821
+ className: panel_module_css_default.scanList,
3822
+ children: paged.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3823
+ className: panel_module_css_default.empty,
3824
+ style: { padding: 20 },
3825
+ children: "无匹配技能"
3826
+ }) : paged.map((entry) => {
3827
+ const existing = isExisting(entry);
3828
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3829
+ className: panel_module_css_default.row + (existing ? " " + panel_module_css_default.rowMuted : ""),
3830
+ children: [
3831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3832
+ type: "checkbox",
3833
+ checked: repoSelected.has(entry.path),
3834
+ disabled: existing,
3835
+ onChange: (event) => {
3836
+ toggleRepoSelected(entry.path, event.target.checked);
3837
+ }
3838
+ }),
3839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3840
+ className: panel_module_css_default.rowMain,
3841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3842
+ className: panel_module_css_default.rowName,
3843
+ children: entry.name
3844
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3845
+ className: panel_module_css_default.rowDesc,
3846
+ children: [
3847
+ entry.dir,
3848
+ " · ",
3849
+ tt("repo.files", {
3850
+ count: entry.fileCount,
3851
+ size: formatBytes(entry.totalBytes)
3852
+ })
3853
+ ]
3854
+ })]
3855
+ }),
3856
+ existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3857
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
3858
+ children: tt("repo.existing")
3859
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3860
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
3861
+ children: entry.origin
3862
+ })
3863
+ ]
3864
+ }, entry.path);
3865
+ })
3866
+ }),
3867
+ visibleCount < filtered.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3868
+ style: {
3869
+ textAlign: "center",
3870
+ marginTop: 8
3871
+ },
3872
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3873
+ type: "button",
3874
+ className: panel_module_css_default.button,
3875
+ onClick: () => setVisibleCount((n) => n + 50),
3876
+ children: [
3877
+ "加载更多 50 (剩余 ",
3878
+ filtered.length - visibleCount,
3879
+ ")"
3880
+ ]
3881
+ })
3882
+ }) : null,
3883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3884
+ className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
3885
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3886
+ type: "button",
3887
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3888
+ disabled: selected.length === 0 || repoImporting,
3889
+ onClick: () => {
3890
+ importRepo();
3891
+ },
3892
+ children: repoImporting ? tt("repo.importing") : `${tt("repo.import", { count: selected.length })} · ${formatBytes(selectedBytes)}`
3893
+ }), repoImporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3894
+ type: "button",
3895
+ className: panel_module_css_default.button,
3896
+ onClick: () => {
3897
+ cancelImport();
3898
+ },
3899
+ children: tt("repo.cancel")
3900
+ }) : null]
3901
+ }),
3902
+ repoResult !== null && repoResult.status === "running" ? (() => {
3903
+ const pct = repoResult.totalBytes > 0 ? Math.min(100, Math.round(repoResult.downloadedBytes / repoResult.totalBytes * 100)) : repoResult.total > 0 ? Math.round(repoResult.done / repoResult.total * 100) : 0;
3904
+ const speed = formatSpeed(repoResult.bytesPerSecond);
3905
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3906
+ className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
3907
+ children: [
3908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3909
+ style: {
3910
+ display: "flex",
3911
+ alignItems: "center",
3912
+ gap: 8,
3913
+ marginBottom: 6
3914
+ },
3915
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3916
+ className: panel_module_css_default.scanProgressTrack,
3917
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3918
+ className: panel_module_css_default.scanProgressFill,
3919
+ style: { width: `${pct}%` }
3920
+ })
3921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3922
+ style: {
3923
+ fontSize: 11,
3924
+ opacity: .75,
3925
+ minWidth: 32,
3926
+ textAlign: "right"
3927
+ },
3928
+ children: [pct, "%"]
3929
+ })]
3930
+ }),
3931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3932
+ className: panel_module_css_default.hintLine,
3933
+ children: [
3934
+ formatBytes(repoResult.downloadedBytes),
3935
+ " / ",
3936
+ formatBytes(repoResult.totalBytes),
3937
+ speed !== "" ? ` · ${speed}` : "",
3938
+ " · ",
3939
+ repoResult.done,
3940
+ "/",
3941
+ repoResult.total,
3942
+ " 个技能"
3943
+ ]
3944
+ }),
3945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3946
+ className: panel_module_css_default.hintLine,
3947
+ style: { marginTop: 4 },
3948
+ children: [repoResult.current !== void 0 ? tt("repo.importingCurrent", {
3949
+ name: repoResult.current,
3950
+ done: repoResult.done + 1,
3951
+ total: repoResult.total
3952
+ }) : tt("repo.importing"), repoResult.currentFile !== void 0 ? ` · ${repoResult.currentFile}` : ""]
3953
+ }),
3954
+ repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3955
+ className: panel_module_css_default.errorBanner,
3956
+ style: { marginTop: 6 },
3957
+ children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
3958
+ }) : null
3959
+ ]
3960
+ });
3961
+ })() : null
3962
+ ] })] });
3963
+ })() : null,
3964
+ repoResult !== null && repoResult.status !== "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3965
+ className: repoResult.status === "cancelled" ? panel_module_css_default.errorBanner + " " + panel_module_css_default.actionsTop : panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
3966
+ children: [
3967
+ repoResult.status === "cancelled" ? tt("repo.cancelled", {
3968
+ imported: repoResult.imported.length,
3969
+ total: repoResult.total
3970
+ }) : `${tt("repo.imported", { count: repoResult.imported.length })} · ${tt("repo.skippedExisting", { count: repoResult.skipped.length })} · ${tt("repo.failed", { count: repoResult.failed.length })}`,
3971
+ repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3972
+ style: { marginTop: 6 },
3973
+ children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
3974
+ }) : null,
3975
+ repoResult.status === "error" && repoResult.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3976
+ style: { marginTop: 6 },
3977
+ children: repoResult.error
3978
+ }) : null
3979
+ ]
3980
+ }) : null
3981
+ ]
3982
+ })]
3357
3983
  }) : null,
3358
3984
  updateAllDialog ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
3359
3985
  title: tt("market.updateAllConfirmTitle"),
@@ -3420,6 +4046,8 @@ window.__ModuleLoader__.load({
3420
4046
  const [repoSelected, setRepoSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
3421
4047
  const [repoImporting, setRepoImporting] = (0, react.useState)(false);
3422
4048
  const [repoResult, setRepoResult] = (0, react.useState)(null);
4049
+ const [importJobId, setImportJobId] = (0, react.useState)(null);
4050
+ const pollAbortRef = (0, react.useRef)(null);
3423
4051
  const [search, setSearch] = (0, react.useState)("");
3424
4052
  /** 工作区(项目)路径;空 = 只看用户级技能。 */
3425
4053
  const [workspace, setWorkspace] = (0, react.useState)("");
@@ -3458,6 +4086,7 @@ window.__ModuleLoader__.load({
3458
4086
  const [conflictDialog, setConflictDialog] = (0, react.useState)(null);
3459
4087
  const [confirmDialog, setConfirmDialog] = (0, react.useState)(null);
3460
4088
  const [deleteSkillDialog, setDeleteSkillDialog] = (0, react.useState)(null);
4089
+ const [deleteGroupDialog, setDeleteGroupDialog] = (0, react.useState)(null);
3461
4090
  const [confirmClearTrash, setConfirmClearTrash] = (0, react.useState)(false);
3462
4091
  /** 「全部更新」确认对话框(市场 tab)。 */
3463
4092
  const [updateAllDialog, setUpdateAllDialog] = (0, react.useState)(false);
@@ -3472,6 +4101,7 @@ window.__ModuleLoader__.load({
3472
4101
  /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
3473
4102
  const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
3474
4103
  const [showLegend, setShowLegend] = (0, react.useState)(false);
4104
+ const [editMode, setEditMode] = (0, react.useState)(false);
3475
4105
  const toggleGroupCollapse = (0, react.useCallback)((key) => {
3476
4106
  setCollapsedGroups((previous) => {
3477
4107
  const next = new Set(previous);
@@ -3848,6 +4478,43 @@ window.__ModuleLoader__.load({
3848
4478
  loadGroups,
3849
4479
  loadSources
3850
4480
  ]);
4481
+ /** 打开整组删除确认(来源分组一键删除)。 */
4482
+ const requestDeleteGroup = (0, react.useCallback)((name, skillNames) => {
4483
+ setDeleteGroupDialog({
4484
+ name,
4485
+ skillNames
4486
+ });
4487
+ }, []);
4488
+ /** 执行整组删除(逐个移入回收站,跳过只读)。 */
4489
+ const runDeleteGroup = (0, react.useCallback)(async () => {
4490
+ const dialog = deleteGroupDialog;
4491
+ if (dialog === null) return;
4492
+ setDeleteGroupDialog(null);
4493
+ setTagBusy(true);
4494
+ setLoadError(null);
4495
+ const failures = [];
4496
+ let done = 0;
4497
+ for (const name of dialog.skillNames) try {
4498
+ await api.deleteSkill(name);
4499
+ done += 1;
4500
+ } catch (error) {
4501
+ failures.push(name + ": " + errorMessage(error));
4502
+ }
4503
+ await Promise.all([
4504
+ load(),
4505
+ loadGroups(),
4506
+ loadSources()
4507
+ ]);
4508
+ setTagBusy(false);
4509
+ if (failures.length > 0) setLoadError(`删除整组 "${dialog.name}":成功 ${done} 个,失败 ${failures.length} 个:` + failures.join("; "));
4510
+ else if (done > 0) setSuccessBanner(`已删除整组 "${dialog.name}":${done} 个技能已移入回收站`);
4511
+ }, [
4512
+ api,
4513
+ deleteGroupDialog,
4514
+ load,
4515
+ loadGroups,
4516
+ loadSources
4517
+ ]);
3851
4518
  /** 新建一个空 tag 分组。 */
3852
4519
  const createTag = (0, react.useCallback)(async (event) => {
3853
4520
  event.preventDefault();
@@ -3902,6 +4569,89 @@ window.__ModuleLoader__.load({
3902
4569
  setTagBusy(false);
3903
4570
  }
3904
4571
  }, [api, applyTags]);
4572
+ /** 拖拽重排场景分组 */
4573
+ const reorderTags = (0, react.useCallback)(async (orderedIds) => {
4574
+ setTagBusy(true);
4575
+ setLoadError(null);
4576
+ try {
4577
+ const tags = await api.reorderTags(orderedIds);
4578
+ applyTags(tags);
4579
+ } catch (error) {
4580
+ const msg = errorMessage(error);
4581
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
4582
+ const byId = new Map(groupsState?.tags.map((t) => [t.id, t]) ?? []);
4583
+ const reordered = orderedIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
4584
+ if (reordered.length === orderedIds.length) {
4585
+ applyTags(reordered);
4586
+ setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
4587
+ } else setLoadError(msg);
4588
+ } else setLoadError(msg);
4589
+ } finally {
4590
+ setTagBusy(false);
4591
+ }
4592
+ }, [
4593
+ api,
4594
+ applyTags,
4595
+ groupsState
4596
+ ]);
4597
+ /** 拖拽重排来源集合 */
4598
+ const reorderCollections = (0, react.useCallback)(async (orderedNames) => {
4599
+ setTagBusy(true);
4600
+ setLoadError(null);
4601
+ try {
4602
+ const collections = await api.reorderCollections(orderedNames);
4603
+ setGroupsState((prev) => prev === null ? prev : {
4604
+ ...prev,
4605
+ collections
4606
+ });
4607
+ loadGroups();
4608
+ } catch (error) {
4609
+ const msg = errorMessage(error);
4610
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
4611
+ setGroupsState((prev) => {
4612
+ if (prev === null) return prev;
4613
+ const map = new Map(prev.collections.map((c) => [c.name, c]));
4614
+ const reordered = orderedNames.map((n) => map.get(n)).filter((c) => c !== void 0);
4615
+ for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
4616
+ return {
4617
+ ...prev,
4618
+ collections: reordered
4619
+ };
4620
+ });
4621
+ setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
4622
+ } else setLoadError(msg);
4623
+ } finally {
4624
+ setTagBusy(false);
4625
+ }
4626
+ }, [api, loadGroups]);
4627
+ /** 拖拽重排来源顶层分组(project / col:xxx / personal 全量可拖) */
4628
+ const reorderSourceGroups = (0, react.useCallback)(async (orderedKeys) => {
4629
+ setTagBusy(true);
4630
+ setLoadError(null);
4631
+ try {
4632
+ await api.reorderSourceGroups(orderedKeys);
4633
+ await loadGroups();
4634
+ } catch (error) {
4635
+ const msg = errorMessage(error);
4636
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
4637
+ setGroupsState((prev) => {
4638
+ if (prev === null) return prev;
4639
+ const colOrder = orderedKeys.filter((k) => k.startsWith("col:")).map((k) => k.slice(4));
4640
+ const map = new Map(prev.collections.map((c) => [c.name, c]));
4641
+ const reordered = colOrder.map((n) => map.get(n)).filter((c) => c !== void 0);
4642
+ for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
4643
+ return {
4644
+ ...prev,
4645
+ collections: reordered,
4646
+ sourceGroupOrder: orderedKeys
4647
+ };
4648
+ });
4649
+ setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
4650
+ } else setLoadError(msg);
4651
+ } finally {
4652
+ setTagBusy(false);
4653
+ }
4654
+ }, [api, loadGroups]);
3905
4655
  /** 添加一个市场源(内置市场目录与手动输入共用),并立即扫描它。 */
3906
4656
  const addSource = (0, react.useCallback)(async (input) => {
3907
4657
  const value = input.trim();
@@ -4018,15 +4768,50 @@ window.__ModuleLoader__.load({
4018
4768
  return next;
4019
4769
  });
4020
4770
  }, []);
4021
- /** Import every checked, non-existing repo skill, then refresh the hub. */
4771
+ /** Import every checked, non-existing repo skill (B方案:job+轮询+进度). */
4022
4772
  const importRepo = (0, react.useCallback)(async () => {
4023
4773
  if (repoDiscoverState.status !== "ready") return;
4024
4774
  setRepoImporting(true);
4025
4775
  setRepoResult(null);
4776
+ setImportJobId(null);
4026
4777
  setLoadError(null);
4778
+ let finalProgress = null;
4027
4779
  try {
4028
- const result = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? void 0);
4029
- setRepoResult(result);
4780
+ const created = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? void 0);
4781
+ setImportJobId(created.jobId);
4782
+ setRepoResult({
4783
+ ok: true,
4784
+ jobId: created.jobId,
4785
+ status: "running",
4786
+ total: created.total,
4787
+ done: 0,
4788
+ totalBytes: created.totalBytes,
4789
+ downloadedBytes: 0,
4790
+ imported: [],
4791
+ skipped: [],
4792
+ failed: []
4793
+ });
4794
+ pollAbortRef.current?.abort();
4795
+ pollAbortRef.current = new AbortController();
4796
+ const signal = pollAbortRef.current.signal;
4797
+ let attempt = 0;
4798
+ for (;;) {
4799
+ if (signal.aborted) break;
4800
+ const delay = Math.min(2e3, 800 + attempt * 200);
4801
+ await new Promise((r) => setTimeout(r, delay));
4802
+ if (signal.aborted) break;
4803
+ try {
4804
+ const progress = await api.repoImportProgress(created.jobId);
4805
+ setRepoResult(progress);
4806
+ finalProgress = progress;
4807
+ if (progress.status !== "running") break;
4808
+ attempt = 0;
4809
+ } catch (pollError) {
4810
+ if (errorMessage(pollError).includes("not found")) break;
4811
+ attempt += 1;
4812
+ if (attempt > 8) break;
4813
+ }
4814
+ }
4030
4815
  await Promise.all([
4031
4816
  load(),
4032
4817
  loadMarket(),
@@ -4036,6 +4821,28 @@ window.__ModuleLoader__.load({
4036
4821
  } catch (error) {
4037
4822
  setLoadError(errorMessage(error));
4038
4823
  } finally {
4824
+ if (finalProgress !== null && (finalProgress.imported.length > 0 || finalProgress.skipped.length > 0)) {
4825
+ const doneNames = /* @__PURE__ */ new Set([...finalProgress.imported.map((r) => r.name), ...finalProgress.skipped.map((r) => r.name)]);
4826
+ const donePaths = new Set(repoDiscoverState.data.entries.filter((e) => doneNames.has(e.name)).map((e) => e.path));
4827
+ setRepoSelected((prev) => {
4828
+ const next = new Set(prev);
4829
+ for (const p of donePaths) next.delete(p);
4830
+ return next;
4831
+ });
4832
+ setRepoDiscoverState((prev) => {
4833
+ if (prev.status !== "ready") return prev;
4834
+ return {
4835
+ ...prev,
4836
+ data: {
4837
+ ...prev.data,
4838
+ entries: prev.data.entries.map((e) => doneNames.has(e.name) ? {
4839
+ ...e,
4840
+ existing: true
4841
+ } : e)
4842
+ }
4843
+ };
4844
+ });
4845
+ }
4039
4846
  setRepoImporting(false);
4040
4847
  }
4041
4848
  }, [
@@ -4047,6 +4854,41 @@ window.__ModuleLoader__.load({
4047
4854
  loadGroups,
4048
4855
  loadSources
4049
4856
  ]);
4857
+ (0, react.useEffect)(() => {
4858
+ return () => {
4859
+ pollAbortRef.current?.abort();
4860
+ };
4861
+ }, []);
4862
+ /** 取消正在进行的导入(选项2:唯有取消才停) */
4863
+ const cancelImport = (0, react.useCallback)(async () => {
4864
+ if (importJobId === null) return;
4865
+ pollAbortRef.current?.abort();
4866
+ try {
4867
+ const res = await api.repoImportCancel(importJobId);
4868
+ try {
4869
+ const progress = await api.repoImportProgress(importJobId);
4870
+ setRepoResult(progress);
4871
+ } catch {
4872
+ setRepoResult((prev) => prev !== null ? {
4873
+ ...prev,
4874
+ status: res.status
4875
+ } : prev);
4876
+ }
4877
+ } catch (error) {
4878
+ setLoadError(errorMessage(error));
4879
+ } finally {
4880
+ setRepoImporting(false);
4881
+ }
4882
+ }, [api, importJobId]);
4883
+ /** 清空扫描结果(关闭归属卡片) */
4884
+ const clearScan = (0, react.useCallback)(() => {
4885
+ setRepoDiscoverState({ status: "idle" });
4886
+ setScanningRepo(null);
4887
+ setRepoResult(null);
4888
+ setRepoSelected(/* @__PURE__ */ new Set());
4889
+ setImportJobId(null);
4890
+ pollAbortRef.current?.abort();
4891
+ }, []);
4050
4892
  /** 检查所有市场源的上游更新(服务端节流)。 */
4051
4893
  const checkMarket = (0, react.useCallback)(async () => {
4052
4894
  try {
@@ -4211,6 +5053,7 @@ window.__ModuleLoader__.load({
4211
5053
  repoSelected,
4212
5054
  repoImporting,
4213
5055
  repoResult,
5056
+ importJobId,
4214
5057
  search,
4215
5058
  workspace,
4216
5059
  detail,
@@ -4245,6 +5088,7 @@ window.__ModuleLoader__.load({
4245
5088
  conflictDialog,
4246
5089
  confirmDialog,
4247
5090
  deleteSkillDialog,
5091
+ deleteGroupDialog,
4248
5092
  confirmClearTrash,
4249
5093
  updateAllDialog,
4250
5094
  editingTag,
@@ -4256,6 +5100,7 @@ window.__ModuleLoader__.load({
4256
5100
  collapsedGroups,
4257
5101
  subdividedProjects,
4258
5102
  showLegend,
5103
+ editMode,
4259
5104
  actionNames,
4260
5105
  viewNames,
4261
5106
  normalized,
@@ -4288,6 +5133,7 @@ window.__ModuleLoader__.load({
4288
5133
  setConflictDialog,
4289
5134
  setConfirmDialog,
4290
5135
  setDeleteSkillDialog,
5136
+ setDeleteGroupDialog,
4291
5137
  setConfirmClearTrash,
4292
5138
  setUpdateAllDialog,
4293
5139
  setEditingTag,
@@ -4296,6 +5142,7 @@ window.__ModuleLoader__.load({
4296
5142
  setNewTagName,
4297
5143
  setEditSearch,
4298
5144
  setShowLegend,
5145
+ setEditMode,
4299
5146
  toggleGroupCollapse,
4300
5147
  toggleSubdivide,
4301
5148
  checkUpdate,
@@ -4314,9 +5161,14 @@ window.__ModuleLoader__.load({
4314
5161
  clearTrash,
4315
5162
  requestDeleteSkill,
4316
5163
  runDeleteSkill,
5164
+ requestDeleteGroup,
5165
+ runDeleteGroup,
4317
5166
  createTag,
4318
5167
  deleteTag,
4319
5168
  saveTag,
5169
+ reorderTags,
5170
+ reorderCollections,
5171
+ reorderSourceGroups,
4320
5172
  addSource,
4321
5173
  addMarketSource,
4322
5174
  removeMarketSource,
@@ -4324,6 +5176,8 @@ window.__ModuleLoader__.load({
4324
5176
  confirmBranchChoice,
4325
5177
  toggleRepoSelected,
4326
5178
  importRepo,
5179
+ cancelImport,
5180
+ clearScan,
4327
5181
  checkMarket,
4328
5182
  syncMarketSource,
4329
5183
  confirmMarketSync,
@@ -4350,7 +5204,7 @@ window.__ModuleLoader__.load({
4350
5204
  const hub = useSkillHub(props.api);
4351
5205
  /** 工作区输入草稿:回车才应用,避免每次按键都触发目录重拉。 */
4352
5206
  const [workspaceDraft, setWorkspaceDraft] = (0, react.useState)("");
4353
- const { catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc, formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search, workspace, setWorkspace, sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered, conflictDialog, confirmDialog, deleteSkillDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog, syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource, showLegend, setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab, setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch, setShowLegend, checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled } = hub;
5207
+ const { catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc, formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search, workspace, setWorkspace, sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered, conflictDialog, confirmDialog, deleteSkillDialog, deleteGroupDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog, syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource, showLegend, editMode, setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab, setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setDeleteGroupDialog, setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch, setShowLegend, setEditMode, checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runDeleteGroup, runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled } = hub;
4354
5208
  if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
4355
5209
  detail,
4356
5210
  hubConfig,
@@ -4741,6 +5595,13 @@ window.__ModuleLoader__.load({
4741
5595
  setSearch(event.target.value);
4742
5596
  },
4743
5597
  placeholder: tt("panel.search")
5598
+ }),
5599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5600
+ type: "button",
5601
+ className: panel_module_css_default.button + (editMode ? " " + panel_module_css_default.primary : ""),
5602
+ style: { marginLeft: "auto" },
5603
+ onClick: () => setEditMode((v) => !v),
5604
+ children: editMode ? "完成" : "编辑"
4744
5605
  })
4745
5606
  ]
4746
5607
  }),
@@ -4791,19 +5652,6 @@ window.__ModuleLoader__.load({
4791
5652
  })]
4792
5653
  }, entry.name))]
4793
5654
  }) : null,
4794
- catalog.disabled.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4795
- className: panel_module_css_default.section,
4796
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4797
- className: panel_module_css_default.sectionTitle,
4798
- children: tt("panel.disabled")
4799
- }), catalog.disabled.filter((record) => (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter)).map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
4800
- record,
4801
- busy: busyNames.has(record.name),
4802
- onEnable: () => {
4803
- enableDisabled(record);
4804
- }
4805
- }, record.name))]
4806
- }) : null,
4807
5655
  catalog.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4808
5656
  className: panel_module_css_default.section,
4809
5657
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -4895,6 +5743,22 @@ window.__ModuleLoader__.load({
4895
5743
  runDeleteSkill();
4896
5744
  }
4897
5745
  }) : null,
5746
+ deleteGroupDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
5747
+ title: tt("source.deleteGroupTitle"),
5748
+ text: tt("source.deleteGroupText", {
5749
+ name: deleteGroupDialog.name,
5750
+ count: deleteGroupDialog.skillNames.length
5751
+ }),
5752
+ items: deleteGroupDialog.skillNames,
5753
+ confirmLabel: tt("source.deleteGroup"),
5754
+ danger: true,
5755
+ onCancel: () => {
5756
+ setDeleteGroupDialog(null);
5757
+ },
5758
+ onConfirm: () => {
5759
+ runDeleteGroup();
5760
+ }
5761
+ }) : null,
4898
5762
  confirmClearTrash ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
4899
5763
  title: tt("source.clearTrashConfirmTitle"),
4900
5764
  text: tt("source.clearTrashConfirmText"),