dsh-skill-hub 0.2.6 → 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": "清空回收站",
@@ -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",
@@ -1846,7 +1913,7 @@ window.__ModuleLoader__.load({
1846
1913
  }
1847
1914
  //#endregion
1848
1915
  //#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/panel/panel.module.css.mjs
1849
- 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}";
1850
1917
  const tagId = "dsh-skill-hub/panel.module.css";
1851
1918
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1852
1919
  const tag = document.createElement("style");
@@ -1895,6 +1962,9 @@ window.__ModuleLoader__.load({
1895
1962
  "dot": "_6VtqdG_dot",
1896
1963
  "dotModel": "_6VtqdG_dotModel",
1897
1964
  "dotUser": "_6VtqdG_dotUser",
1965
+ "dragHandle": "_6VtqdG_dragHandle",
1966
+ "dragOver": "_6VtqdG_dragOver",
1967
+ "dragging": "_6VtqdG_dragging",
1898
1968
  "empty": "_6VtqdG_empty",
1899
1969
  "errorBanner": "_6VtqdG_errorBanner",
1900
1970
  "filterBar": "_6VtqdG_filterBar",
@@ -1937,6 +2007,9 @@ window.__ModuleLoader__.load({
1937
2007
  "rowName": "_6VtqdG_rowName",
1938
2008
  "rowNameText": "_6VtqdG_rowNameText",
1939
2009
  "rowStatic": "_6VtqdG_rowStatic",
2010
+ "scanList": "_6VtqdG_scanList",
2011
+ "scanProgressFill": "_6VtqdG_scanProgressFill",
2012
+ "scanProgressTrack": "_6VtqdG_scanProgressTrack",
1940
2013
  "search": "_6VtqdG_search",
1941
2014
  "section": "_6VtqdG_section",
1942
2015
  "sectionHeadRow": "_6VtqdG_sectionHeadRow",
@@ -2369,17 +2442,6 @@ window.__ModuleLoader__.load({
2369
2442
  children: skill.description
2370
2443
  })]
2371
2444
  }), skill.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2372
- type: "button",
2373
- className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger + " " + panel_module_css_default.iconBtn,
2374
- disabled: hub.busyNames.has(skill.name) || hub.tagBusy,
2375
- title: tt("row.delete"),
2376
- "aria-label": tt("row.delete"),
2377
- onClick: (event) => {
2378
- event.stopPropagation();
2379
- hub.requestDeleteSkill(skill.name);
2380
- },
2381
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutline16, { size: 14 })
2382
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2383
2445
  type: "button",
2384
2446
  role: "switch",
2385
2447
  "aria-checked": true,
@@ -2391,7 +2453,18 @@ window.__ModuleLoader__.load({
2391
2453
  hub.toggle(skill, false);
2392
2454
  },
2393
2455
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2394
- })] }) : /* @__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", {
2395
2468
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
2396
2469
  children: tt("row.readonly")
2397
2470
  })]
@@ -2463,219 +2536,326 @@ window.__ModuleLoader__.load({
2463
2536
  }
2464
2537
  //#endregion
2465
2538
  //#region src/client/panel/SourcesView.tsx
2466
- /** 项目级三级树:项目级 → 具体工作区(可折叠、可细分)→ .dsh/.agents。 */
2467
- 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) {
2468
2547
  const { hub } = props;
2469
- const { sorted, sourceFilter, origins, collapsedGroups, subdividedProjects, toggleGroupCollapse, toggleSubdivide } = hub;
2470
- const projectSkills = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
2471
- if (projectSkills.length === 0) return null;
2472
- const byProject = /* @__PURE__ */ new Map();
2473
- for (const skill of projectSkills) {
2474
- const key = skill.workspace ?? skill.source;
2475
- const entry = byProject.get(key);
2476
- if (entry === void 0) byProject.set(key, {
2477
- title: skill.workspaceTitle ?? skill.workspace ?? tt("groups.project"),
2478
- skills: [skill]
2479
- });
2480
- else entry.skills.push(skill);
2481
- }
2482
- const topCollapsed = collapsedGroups.has("project");
2483
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2484
- className: panel_module_css_default.section,
2485
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2486
- className: panel_module_css_default.groupHead,
2487
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2488
- type: "button",
2489
- className: panel_module_css_default.disclosure,
2490
- "aria-expanded": !topCollapsed,
2491
- onClick: () => {
2492
- 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);
2493
2626
  },
2494
- 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", {
2495
- className: panel_module_css_default.groupTitle,
2496
- children: [
2497
- tt("groups.project"),
2498
- " · ",
2499
- byProject.size
2500
- ]
2501
- })]
2502
- })
2503
- }), !topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
2504
- const projKey = "project:" + key;
2505
- const projCollapsed = collapsedGroups.has(projKey);
2506
- const subdivided = subdividedProjects.has(key);
2507
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2508
- className: panel_module_css_default.projectNest,
2509
2627
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2510
2628
  className: panel_module_css_default.groupHead,
2511
- 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", {
2512
2635
  type: "button",
2513
2636
  className: panel_module_css_default.disclosure,
2514
- "aria-expanded": !projCollapsed,
2637
+ "aria-expanded": !topCollapsed,
2515
2638
  onClick: () => {
2516
- toggleGroupCollapse(projKey);
2639
+ toggleGroupCollapse("project");
2517
2640
  },
2518
- 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", {
2519
2642
  className: panel_module_css_default.groupTitle,
2520
2643
  children: [
2521
- proj.title,
2644
+ tt("groups.project"),
2522
2645
  " · ",
2523
- proj.skills.length,
2524
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2525
- members: proj.skills.map((skill) => skill.name),
2526
- hub
2527
- })
2646
+ byProject.size
2528
2647
  ]
2529
2648
  })]
2530
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2531
- className: panel_module_css_default.groupOps,
2532
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2533
- type: "button",
2534
- className: panel_module_css_default.opBtn,
2535
- onClick: (event) => {
2536
- event.stopPropagation();
2537
- toggleSubdivide(key);
2538
- },
2539
- children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2540
- })
2541
2649
  })]
2542
- }), !projCollapsed ? subdivided ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2543
- className: panel_module_css_default.projectNest,
2544
- children: ["project-dsh", "project-agents"].map((source) => {
2545
- const list = proj.skills.filter((skill) => skill.source === source);
2546
- if (list.length === 0) return null;
2547
- const srcKey = projKey + ":" + source;
2548
- const srcCollapsed = collapsedGroups.has(srcKey);
2549
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2550
- className: panel_module_css_default.projectNest,
2551
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
- className: panel_module_css_default.groupHead,
2553
- 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", {
2554
2680
  type: "button",
2555
- className: panel_module_css_default.disclosure,
2556
- "aria-expanded": !srcCollapsed,
2557
- onClick: () => {
2558
- toggleGroupCollapse(srcKey);
2681
+ className: panel_module_css_default.opBtn,
2682
+ onClick: (event) => {
2683
+ event.stopPropagation();
2684
+ hub.toggleSubdivide(key);
2559
2685
  },
2560
- 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", {
2561
- className: panel_module_css_default.groupTitle,
2562
- children: [
2563
- tt("badge.source." + source),
2564
- " · ",
2565
- list.length
2566
- ]
2567
- })]
2686
+ children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2568
2687
  })
2569
- }), !srcCollapsed ? list.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2570
- skill,
2571
- hub
2572
- }, skill.name)) : null]
2573
- }, srcKey);
2574
- })
2575
- }) : proj.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2576
- skill,
2577
- hub
2578
- }, skill.name)) : null]
2579
- }, projKey);
2580
- }) : null]
2581
- });
2582
- }
2583
- function SourcesView(props) {
2584
- const { hub } = props;
2585
- const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, toggleGroup, enableDisabled } = hub;
2586
- 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, {
2587
- skill,
2588
- hub
2589
- }, skill.name)) });
2590
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2591
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTree, { hub }),
2592
- groupsState !== null && groupsState.collections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2593
- className: panel_module_css_default.empty,
2594
- children: tt("groups.noCollections")
2595
- }) : null,
2596
- 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;
2597
2734
  const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name));
2598
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));
2599
2736
  const collapsed = collapsedGroups.has("col:" + collection.name);
2600
2737
  const view = groupSwitchView(collection.skillNames, viewNames);
2601
2738
  const check = sourceCheck[collection.name];
2602
2739
  const hasWritable = collection.skillNames.some((name) => actionNames.has(name));
2740
+ const isDragging = topDragKey === topKey;
2741
+ const isOver = topOverKey === topKey && topDragKey !== topKey;
2603
2742
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2604
- 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
+ },
2605
2766
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2606
2767
  className: panel_module_css_default.groupHead,
2607
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2608
- type: "button",
2609
- className: panel_module_css_default.disclosure,
2610
- "aria-expanded": !collapsed,
2611
- onClick: () => {
2612
- toggleGroupCollapse("col:" + collection.name);
2613
- },
2614
- 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", {
2615
- 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,
2616
2805
  children: [
2617
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2618
- className: panel_module_css_default.sourceLink,
2619
- href: "https://github.com/" + collection.name,
2620
- target: "_blank",
2621
- 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,
2622
2817
  onClick: (event) => {
2623
2818
  event.stopPropagation();
2819
+ requestSync(collection.name, check.updated);
2624
2820
  },
2625
- children: collection.name
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,
2840
+ onClick: (event) => {
2841
+ event.stopPropagation();
2842
+ toggleGroup("col:" + collection.name, collection.name, view.state);
2843
+ },
2844
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2626
2845
  }),
2627
- " · " + collection.skillNames.length,
2628
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2629
- members: collection.skillNames,
2630
- hub
2631
- })
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
2632
2856
  ]
2633
- })]
2634
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2635
- className: panel_module_css_default.groupOps,
2636
- children: [
2637
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceStatusBadge, {
2638
- check,
2639
- checking: checkingSource === collection.name,
2640
- onCheck: () => {
2641
- checkSources(collection.name);
2642
- }
2643
- }),
2644
- check !== void 0 && check.changed && check.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2645
- type: "button",
2646
- className: panel_module_css_default.opBtn,
2647
- disabled: syncingSource !== null,
2648
- onClick: (event) => {
2649
- event.stopPropagation();
2650
- requestSync(collection.name, check.updated);
2651
- },
2652
- children: syncingSource === collection.name ? tt("source.syncing") : tt("source.sync")
2653
- }) : null,
2654
- check !== void 0 && check.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2655
- type: "button",
2656
- className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2657
- onClick: (event) => {
2658
- event.stopPropagation();
2659
- requestDelete(collection.name, check.deleted);
2660
- },
2661
- children: tt("source.followDelete")
2662
- }) : null,
2663
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2664
- type: "button",
2665
- role: "switch",
2666
- "aria-checked": view.state !== "off",
2667
- "aria-label": collection.name,
2668
- className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
2669
- disabled: batchBusy || collection.skillNames.length === 0 || view.state !== "off" && !hasWritable,
2670
- title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
2671
- onClick: (event) => {
2672
- event.stopPropagation();
2673
- toggleGroup("col:" + collection.name, collection.name, view.state);
2674
- },
2675
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2676
- })
2677
- ]
2678
- })]
2857
+ })
2858
+ ]
2679
2859
  }), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2680
2860
  skill,
2681
2861
  hub
@@ -2687,48 +2867,121 @@ window.__ModuleLoader__.load({
2687
2867
  }
2688
2868
  }, record.name))] }) : null]
2689
2869
  }, "col:" + collection.name);
2690
- }),
2691
- (() => {
2870
+ }
2871
+ if (topKey === "uncategorized-source" && hasPersonal) {
2692
2872
  const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
2693
- 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;
2694
2876
  const collapsed = collapsedGroups.has("uncategorized-source");
2877
+ const isDragging = topDragKey === "uncategorized-source";
2878
+ const isOver = topOverKey === "uncategorized-source" && topDragKey !== "uncategorized-source";
2695
2879
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2696
- className: panel_module_css_default.section,
2697
- 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", {
2698
2904
  className: panel_module_css_default.groupHead,
2699
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2700
- type: "button",
2701
- className: panel_module_css_default.disclosure,
2702
- "aria-expanded": !collapsed,
2703
- onClick: () => {
2704
- toggleGroupCollapse("uncategorized-source");
2705
- },
2706
- 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", {
2707
- className: panel_module_css_default.groupTitle,
2708
- children: [
2709
- tt("groups.personal"),
2710
- " · ",
2711
- uncategorized.length,
2712
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2713
- members: uncategorized.map((skill) => skill.name),
2714
- hub
2715
- })
2716
- ]
2717
- })]
2718
- })
2719
- }), !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, {
2720
2947
  skill,
2721
2948
  hub
2722
- }, skill.name)) : null]
2723
- });
2724
- })()
2725
- ] });
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
+ })] });
2726
2960
  }
2727
2961
  //#endregion
2728
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
+ */
2729
2969
  function ScenesView(props) {
2730
2970
  const { hub } = props;
2731
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
+ };
2732
2985
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2733
2986
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
2734
2987
  className: panel_module_css_default.form,
@@ -2762,56 +3015,89 @@ window.__ModuleLoader__.load({
2762
3015
  const collapsed = collapsedGroups.has("tag:" + tag.id);
2763
3016
  const view = groupSwitchView(tag.skillNames, viewNames);
2764
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;
2765
3020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2766
- 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
+ },
2767
3044
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2768
3045
  className: panel_module_css_default.groupHead,
2769
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2770
- type: "button",
2771
- className: panel_module_css_default.disclosure,
2772
- "aria-expanded": !collapsed,
2773
- onClick: () => {
2774
- toggleGroupCollapse("tag:" + tag.id);
2775
- },
2776
- 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", {
2777
- className: panel_module_css_default.groupTitle,
2778
- children: [
2779
- tag.name,
2780
- " · ",
2781
- tag.skillNames.length,
2782
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2783
- members: tag.skillNames,
2784
- hub
2785
- })
2786
- ]
2787
- })]
2788
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2789
- className: panel_module_css_default.groupOps,
2790
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2791
- type: "button",
2792
- role: "switch",
2793
- "aria-checked": view.state !== "off",
2794
- "aria-label": tag.name,
2795
- className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
2796
- disabled: batchBusy || tag.skillNames.length === 0 || view.state !== "off" && !hasWritable,
2797
- title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
2798
- onClick: (event) => {
2799
- event.stopPropagation();
2800
- toggleGroup("tag:" + tag.id, tag.name, view.state);
2801
- },
2802
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
2803
- }), /* @__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", {
2804
3054
  type: "button",
2805
- className: panel_module_css_default.opBtn,
3055
+ className: panel_module_css_default.disclosure,
3056
+ "aria-expanded": !collapsed,
2806
3057
  onClick: () => {
2807
- setEditingTag(tag);
2808
- setEditName(tag.name);
2809
- setMembersDraft(new Set(tag.skillNames));
2810
- setEditSearch("");
3058
+ toggleGroupCollapse("tag:" + tag.id);
2811
3059
  },
2812
- children: tt("groups.edit")
2813
- })]
2814
- })]
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
+ ]
2815
3101
  }), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2816
3102
  skill,
2817
3103
  hub
@@ -3064,17 +3350,66 @@ window.__ModuleLoader__.load({
3064
3350
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
3065
3351
  return (bytes / 1024 / 1024).toFixed(1) + " MB";
3066
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
+ }
3067
3359
  function MarketView(props) {
3068
3360
  const { hub } = props;
3069
- 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;
3070
3362
  /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
3071
3363
  const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
3072
- /** 一个已添加来源的完整行:状态徽章 + 操作按钮。 */
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 方案:扫描主按钮 + 智能更新按钮(检查/更新二合一)+ 溢出移除 */
3073
3375
  const sourceRow = (record) => {
3074
3376
  const releaseCheck = marketCheck[record.repo];
3075
3377
  const skillCheck = sourceCheck[record.repo];
3076
3378
  const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
3077
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
+ }
3078
3413
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3079
3414
  className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
3080
3415
  children: [
@@ -3098,7 +3433,7 @@ window.__ModuleLoader__.load({
3098
3433
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
3099
3434
  children: tt("market.installed", { count: installedCount })
3100
3435
  }) : null,
3101
- skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3436
+ hasSkillUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3102
3437
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
3103
3438
  children: tt("market.updatable", { count: skillCheck.updated.length })
3104
3439
  }) : null,
@@ -3106,7 +3441,7 @@ window.__ModuleLoader__.load({
3106
3441
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
3107
3442
  children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
3108
3443
  }) : null,
3109
- releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3444
+ hasReleaseUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3110
3445
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
3111
3446
  children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
3112
3447
  }) : null
@@ -3115,40 +3450,39 @@ window.__ModuleLoader__.load({
3115
3450
  }),
3116
3451
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3117
3452
  type: "button",
3118
- className: panel_module_css_default.opBtn,
3119
- disabled: scanning,
3120
- onClick: () => {
3121
- checkMarket();
3122
- },
3123
- children: tt("market.check")
3124
- }),
3125
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3126
- type: "button",
3127
- className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
3128
- disabled: syncingMarket === record.repo,
3129
- onClick: () => {
3130
- syncMarketSource(record.repo);
3453
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3454
+ style: {
3455
+ padding: "4px 10px",
3456
+ fontSize: 12
3131
3457
  },
3132
- children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
3133
- }),
3134
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3135
- type: "button",
3136
- className: panel_module_css_default.opBtn,
3137
3458
  disabled: scanning,
3138
3459
  onClick: () => {
3139
3460
  scanRepo(record.repo);
3140
3461
  },
3141
3462
  children: scanning ? tt("market.scanning") : tt("market.scan")
3142
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
+ }),
3143
3472
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3144
3473
  type: "button",
3145
3474
  className: panel_module_css_default.opBtn,
3146
3475
  disabled: tagBusy,
3147
3476
  title: tt("market.removeHint"),
3477
+ "aria-label": tt("market.deleteSource"),
3148
3478
  onClick: () => {
3149
3479
  removeMarketSource(record.repo);
3150
3480
  },
3151
- children: tt("market.deleteSource")
3481
+ style: {
3482
+ padding: "4px 8px",
3483
+ minWidth: 28
3484
+ },
3485
+ children: "×"
3152
3486
  })
3153
3487
  ]
3154
3488
  });
@@ -3273,109 +3607,379 @@ window.__ModuleLoader__.load({
3273
3607
  marketState.status !== "loading" ? rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: row.element }, row.key)) : null
3274
3608
  ]
3275
3609
  }),
3276
- repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3277
- className: panel_module_css_default.empty,
3278
- children: tt("market.scanning")
3279
- }) : null,
3280
- repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3281
- className: panel_module_css_default.errorBanner,
3282
- children: repoDiscoverState.message
3283
- }) : null,
3284
- repoDiscoverState.status === "ready" ? (() => {
3285
- const entries = repoDiscoverState.data.entries;
3286
- const selected = entries.filter((entry) => repoSelected.has(entry.path) && !entry.existing);
3287
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3288
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
3289
- children: tt("repo.ready", { count: entries.length })
3290
- }), entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3291
- className: panel_module_css_default.empty,
3292
- children: tt("repo.empty")
3293
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3294
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3295
- className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsBottom,
3296
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3297
- type: "button",
3298
- className: panel_module_css_default.button,
3299
- onClick: () => {
3300
- setRepoSelected(new Set(entries.filter((entry) => !entry.existing).map((entry) => entry.path)));
3301
- },
3302
- children: tt("repo.selectAll")
3303
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3304
- type: "button",
3305
- className: panel_module_css_default.button,
3306
- onClick: () => {
3307
- setRepoSelected(/* @__PURE__ */ new Set());
3308
- },
3309
- children: tt("repo.clearAll")
3310
- })]
3311
- }),
3312
- ["skills", "design-templates"].map((root) => {
3313
- const rootEntries = entries.filter((entry) => entry.root === root);
3314
- if (rootEntries.length === 0) return null;
3315
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3316
- className: panel_module_css_default.section,
3317
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3318
- className: panel_module_css_default.sectionTitle,
3319
- children: root === "skills" ? tt("repo.root.skills") : tt("repo.root.designTemplates")
3320
- }), rootEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3321
- 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
+ },
3322
3640
  children: [
3323
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3324
- type: "checkbox",
3325
- checked: repoSelected.has(entry.path),
3326
- disabled: entry.existing,
3327
- onChange: (event) => {
3328
- toggleRepoSelected(entry.path, event.target.checked);
3329
- }
3330
- }),
3331
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3332
- className: panel_module_css_default.rowMain,
3333
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3334
- className: panel_module_css_default.rowName,
3335
- children: entry.name
3336
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3337
- className: panel_module_css_default.rowDesc,
3338
- children: [
3339
- entry.dir,
3340
- " · ",
3341
- tt("repo.files", {
3342
- count: entry.fileCount,
3343
- size: formatBytes(entry.totalBytes)
3344
- })
3345
- ]
3346
- })]
3347
- }),
3348
- entry.existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3349
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
3350
- children: tt("repo.existing")
3351
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3352
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
3353
- children: entry.origin
3354
- })
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
3355
3650
  ]
3356
- }, entry.path))]
3357
- }, root);
3358
- }),
3359
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3651
+ })
3652
+ ]
3653
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3360
3654
  type: "button",
3361
- className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3362
- disabled: selected.length === 0 || repoImporting,
3655
+ className: panel_module_css_default.opBtn,
3363
3656
  onClick: () => {
3364
- importRepo();
3657
+ hub.clearScan();
3365
3658
  },
3366
- children: repoImporting ? tt("repo.importing") : tt("repo.import", { count: selected.length })
3367
- })
3368
- ] })] });
3369
- })() : null,
3370
- repoResult !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3371
- className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
3372
- children: [
3373
- tt("repo.imported", { count: repoResult.imported.length }),
3374
- " · ",
3375
- tt("repo.skippedExisting", { count: repoResult.skipped.length }),
3376
- " · ",
3377
- tt("repo.failed", { count: repoResult.failed.length })
3378
- ]
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
+ })]
3379
3983
  }) : null,
3380
3984
  updateAllDialog ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
3381
3985
  title: tt("market.updateAllConfirmTitle"),
@@ -3442,6 +4046,8 @@ window.__ModuleLoader__.load({
3442
4046
  const [repoSelected, setRepoSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
3443
4047
  const [repoImporting, setRepoImporting] = (0, react.useState)(false);
3444
4048
  const [repoResult, setRepoResult] = (0, react.useState)(null);
4049
+ const [importJobId, setImportJobId] = (0, react.useState)(null);
4050
+ const pollAbortRef = (0, react.useRef)(null);
3445
4051
  const [search, setSearch] = (0, react.useState)("");
3446
4052
  /** 工作区(项目)路径;空 = 只看用户级技能。 */
3447
4053
  const [workspace, setWorkspace] = (0, react.useState)("");
@@ -3480,6 +4086,7 @@ window.__ModuleLoader__.load({
3480
4086
  const [conflictDialog, setConflictDialog] = (0, react.useState)(null);
3481
4087
  const [confirmDialog, setConfirmDialog] = (0, react.useState)(null);
3482
4088
  const [deleteSkillDialog, setDeleteSkillDialog] = (0, react.useState)(null);
4089
+ const [deleteGroupDialog, setDeleteGroupDialog] = (0, react.useState)(null);
3483
4090
  const [confirmClearTrash, setConfirmClearTrash] = (0, react.useState)(false);
3484
4091
  /** 「全部更新」确认对话框(市场 tab)。 */
3485
4092
  const [updateAllDialog, setUpdateAllDialog] = (0, react.useState)(false);
@@ -3494,6 +4101,7 @@ window.__ModuleLoader__.load({
3494
4101
  /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
3495
4102
  const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
3496
4103
  const [showLegend, setShowLegend] = (0, react.useState)(false);
4104
+ const [editMode, setEditMode] = (0, react.useState)(false);
3497
4105
  const toggleGroupCollapse = (0, react.useCallback)((key) => {
3498
4106
  setCollapsedGroups((previous) => {
3499
4107
  const next = new Set(previous);
@@ -3870,6 +4478,43 @@ window.__ModuleLoader__.load({
3870
4478
  loadGroups,
3871
4479
  loadSources
3872
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
+ ]);
3873
4518
  /** 新建一个空 tag 分组。 */
3874
4519
  const createTag = (0, react.useCallback)(async (event) => {
3875
4520
  event.preventDefault();
@@ -3924,6 +4569,89 @@ window.__ModuleLoader__.load({
3924
4569
  setTagBusy(false);
3925
4570
  }
3926
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]);
3927
4655
  /** 添加一个市场源(内置市场目录与手动输入共用),并立即扫描它。 */
3928
4656
  const addSource = (0, react.useCallback)(async (input) => {
3929
4657
  const value = input.trim();
@@ -4040,15 +4768,50 @@ window.__ModuleLoader__.load({
4040
4768
  return next;
4041
4769
  });
4042
4770
  }, []);
4043
- /** Import every checked, non-existing repo skill, then refresh the hub. */
4771
+ /** Import every checked, non-existing repo skill (B方案:job+轮询+进度). */
4044
4772
  const importRepo = (0, react.useCallback)(async () => {
4045
4773
  if (repoDiscoverState.status !== "ready") return;
4046
4774
  setRepoImporting(true);
4047
4775
  setRepoResult(null);
4776
+ setImportJobId(null);
4048
4777
  setLoadError(null);
4778
+ let finalProgress = null;
4049
4779
  try {
4050
- const result = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? void 0);
4051
- 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
+ }
4052
4815
  await Promise.all([
4053
4816
  load(),
4054
4817
  loadMarket(),
@@ -4058,6 +4821,28 @@ window.__ModuleLoader__.load({
4058
4821
  } catch (error) {
4059
4822
  setLoadError(errorMessage(error));
4060
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
+ }
4061
4846
  setRepoImporting(false);
4062
4847
  }
4063
4848
  }, [
@@ -4069,6 +4854,41 @@ window.__ModuleLoader__.load({
4069
4854
  loadGroups,
4070
4855
  loadSources
4071
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
+ }, []);
4072
4892
  /** 检查所有市场源的上游更新(服务端节流)。 */
4073
4893
  const checkMarket = (0, react.useCallback)(async () => {
4074
4894
  try {
@@ -4233,6 +5053,7 @@ window.__ModuleLoader__.load({
4233
5053
  repoSelected,
4234
5054
  repoImporting,
4235
5055
  repoResult,
5056
+ importJobId,
4236
5057
  search,
4237
5058
  workspace,
4238
5059
  detail,
@@ -4267,6 +5088,7 @@ window.__ModuleLoader__.load({
4267
5088
  conflictDialog,
4268
5089
  confirmDialog,
4269
5090
  deleteSkillDialog,
5091
+ deleteGroupDialog,
4270
5092
  confirmClearTrash,
4271
5093
  updateAllDialog,
4272
5094
  editingTag,
@@ -4278,6 +5100,7 @@ window.__ModuleLoader__.load({
4278
5100
  collapsedGroups,
4279
5101
  subdividedProjects,
4280
5102
  showLegend,
5103
+ editMode,
4281
5104
  actionNames,
4282
5105
  viewNames,
4283
5106
  normalized,
@@ -4310,6 +5133,7 @@ window.__ModuleLoader__.load({
4310
5133
  setConflictDialog,
4311
5134
  setConfirmDialog,
4312
5135
  setDeleteSkillDialog,
5136
+ setDeleteGroupDialog,
4313
5137
  setConfirmClearTrash,
4314
5138
  setUpdateAllDialog,
4315
5139
  setEditingTag,
@@ -4318,6 +5142,7 @@ window.__ModuleLoader__.load({
4318
5142
  setNewTagName,
4319
5143
  setEditSearch,
4320
5144
  setShowLegend,
5145
+ setEditMode,
4321
5146
  toggleGroupCollapse,
4322
5147
  toggleSubdivide,
4323
5148
  checkUpdate,
@@ -4336,9 +5161,14 @@ window.__ModuleLoader__.load({
4336
5161
  clearTrash,
4337
5162
  requestDeleteSkill,
4338
5163
  runDeleteSkill,
5164
+ requestDeleteGroup,
5165
+ runDeleteGroup,
4339
5166
  createTag,
4340
5167
  deleteTag,
4341
5168
  saveTag,
5169
+ reorderTags,
5170
+ reorderCollections,
5171
+ reorderSourceGroups,
4342
5172
  addSource,
4343
5173
  addMarketSource,
4344
5174
  removeMarketSource,
@@ -4346,6 +5176,8 @@ window.__ModuleLoader__.load({
4346
5176
  confirmBranchChoice,
4347
5177
  toggleRepoSelected,
4348
5178
  importRepo,
5179
+ cancelImport,
5180
+ clearScan,
4349
5181
  checkMarket,
4350
5182
  syncMarketSource,
4351
5183
  confirmMarketSync,
@@ -4372,7 +5204,7 @@ window.__ModuleLoader__.load({
4372
5204
  const hub = useSkillHub(props.api);
4373
5205
  /** 工作区输入草稿:回车才应用,避免每次按键都触发目录重拉。 */
4374
5206
  const [workspaceDraft, setWorkspaceDraft] = (0, react.useState)("");
4375
- 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;
4376
5208
  if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
4377
5209
  detail,
4378
5210
  hubConfig,
@@ -4763,6 +5595,13 @@ window.__ModuleLoader__.load({
4763
5595
  setSearch(event.target.value);
4764
5596
  },
4765
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 ? "完成" : "编辑"
4766
5605
  })
4767
5606
  ]
4768
5607
  }),
@@ -4813,19 +5652,6 @@ window.__ModuleLoader__.load({
4813
5652
  })]
4814
5653
  }, entry.name))]
4815
5654
  }) : null,
4816
- catalog.disabled.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4817
- className: panel_module_css_default.section,
4818
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4819
- className: panel_module_css_default.sectionTitle,
4820
- children: tt("panel.disabled")
4821
- }), 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, {
4822
- record,
4823
- busy: busyNames.has(record.name),
4824
- onEnable: () => {
4825
- enableDisabled(record);
4826
- }
4827
- }, record.name))]
4828
- }) : null,
4829
5655
  catalog.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4830
5656
  className: panel_module_css_default.section,
4831
5657
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -4917,6 +5743,22 @@ window.__ModuleLoader__.load({
4917
5743
  runDeleteSkill();
4918
5744
  }
4919
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,
4920
5762
  confirmClearTrash ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
4921
5763
  title: tt("source.clearTrashConfirmTitle"),
4922
5764
  text: tt("source.clearTrashConfirmText"),