dsh-skill-hub 0.3.5 → 0.3.7

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.
Files changed (40) hide show
  1. package/lib/client.js +1238 -509
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +401 -47
  4. package/lib/types/client/SkillHubSettingsCard.d.ts +0 -2
  5. package/lib/types/client/api.d.ts +5 -1
  6. package/lib/types/client/helpers.d.ts +10 -0
  7. package/lib/types/client/locales.d.ts +30 -0
  8. package/lib/types/client/panel/DisabledRow.d.ts +2 -0
  9. package/lib/types/client/panel/SkillDetailView.d.ts +5 -1
  10. package/lib/types/client/panel/dialogs.d.ts +19 -0
  11. package/lib/types/client/panel/useSkillHub.d.ts +12 -1
  12. package/lib/types/client/settings-card.d.ts +0 -17
  13. package/lib/types/client/settings-form.d.ts +0 -1
  14. package/lib/types/protocol.d.ts +40 -17
  15. package/lib/types/repo.d.ts +4 -6
  16. package/lib/types/skillfs.d.ts +13 -0
  17. package/package.json +30 -28
  18. package/src/client/SkillHubSettingsCard.tsx +0 -2
  19. package/src/client/api.ts +18 -0
  20. package/src/client/helpers.ts +40 -0
  21. package/src/client/index.tsx +9 -5
  22. package/src/client/locales.ts +60 -0
  23. package/src/client/panel/DisabledRow.tsx +22 -7
  24. package/src/client/panel/MarketView.tsx +8 -1
  25. package/src/client/panel/ScenesView.tsx +1 -1
  26. package/src/client/panel/SkillDetailView.tsx +35 -11
  27. package/src/client/panel/SkillHubPanel.tsx +42 -4
  28. package/src/client/panel/SkillRow.tsx +5 -2
  29. package/src/client/panel/SourcesView.tsx +2 -17
  30. package/src/client/panel/dialogs.tsx +61 -0
  31. package/src/client/panel/panel.module.css +3 -10
  32. package/src/client/panel/useSkillHub.ts +97 -12
  33. package/src/client/settings-card.tsx +0 -42
  34. package/src/client/settings-form.ts +1 -5
  35. package/src/client/slash-dots.tsx +3 -2
  36. package/src/protocol.ts +42 -8
  37. package/src/repo.ts +71 -29
  38. package/src/routes.test.ts +22 -0
  39. package/src/routes.ts +181 -10
  40. package/src/skillfs.ts +188 -3
package/lib/client.js CHANGED
@@ -46,7 +46,9 @@ window.__ModuleLoader__.load({
46
46
  sourceSync: "/api/skill-hub/sources/sync",
47
47
  sourceDelete: "/api/skill-hub/sources/delete",
48
48
  sourceRestore: "/api/skill-hub/sources/restore",
49
- sourceTrashClear: "/api/skill-hub/sources/trash/clear"
49
+ sourceTrashClear: "/api/skill-hub/sources/trash/clear",
50
+ diagnosticFix: "/api/skill-hub/diagnostic/fix",
51
+ marketSourceVersions: "/api/skill-hub/market/source/versions"
50
52
  };
51
53
  /** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
52
54
  function isProjectSource(source) {
@@ -177,6 +179,10 @@ window.__ModuleLoader__.load({
177
179
  body: JSON.stringify({ repo })
178
180
  }));
179
181
  }
182
+ /** Releases + branches of a market source for the version picker. */
183
+ async marketSourceVersions(repo) {
184
+ return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceVersions + "?repo=" + encodeURIComponent(repo)));
185
+ }
180
186
  /** Pin a market source to an explicit ref (branch picker). */
181
187
  async setMarketSourceRef(repo, ref) {
182
188
  return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceRef, {
@@ -338,6 +344,14 @@ window.__ModuleLoader__.load({
338
344
  async clearTrash() {
339
345
  return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceTrashClear, { method: "POST" }));
340
346
  }
347
+ /** Auto-fix a fixable diagnostic (e.g. unquoted colon). */
348
+ async fixDiagnostic(path) {
349
+ return readJson(await fetchWithTimeout(SKILL_HUB_API.diagnosticFix, {
350
+ method: "POST",
351
+ headers: { "content-type": "application/json" },
352
+ body: JSON.stringify({ path })
353
+ }));
354
+ }
341
355
  };
342
356
  //#endregion
343
357
  //#region src/client/locales.ts
@@ -407,12 +421,22 @@ window.__ModuleLoader__.load({
407
421
  "market.branchTitle": "选择分支",
408
422
  "market.branchHint": "该仓库没有正式发布,选择要跟踪的分支(默认为 main):",
409
423
  "market.branchConfirm": "确定",
424
+ "market.versionTitle": "切换跟踪版本",
425
+ "market.versionText": "选择 {repo} 要跟踪的版本,切换后建议重新扫描并同步已装技能",
426
+ "market.versionCurrent": "当前 {ref}",
427
+ "market.versionReleases": "发布版",
428
+ "market.versionBranches": "分支",
429
+ "market.versionCustom": "自定义(tag / 分支 / commit)",
430
+ "market.versionConfirm": "切换到 {ref}",
431
+ "market.versionHint": "点击切换跟踪版本",
432
+ "market.unpinned": "未定版",
410
433
  "market.syncTitle": "同步本地技能",
411
434
  "market.syncText": "市场源已对齐到 {ref},要同步更新以下本地技能吗?",
412
435
  "market.syncNone": "该来源还没有导入本地技能。",
413
436
  "market.syncCancel": "跳过",
414
437
  "market.removeHint": "移除源不影响已安装的技能。",
415
438
  "repo.ready": "发现 {count} 个可导入技能",
439
+ "repo.truncated": "⚠ 仓库过大,仅展示部分技能(GitHub 截断),可缩小仓库或指定子目录",
416
440
  "repo.empty": "没有找到可导入技能。",
417
441
  "repo.selectAll": "全选",
418
442
  "repo.clearAll": "清空",
@@ -485,6 +509,26 @@ window.__ModuleLoader__.load({
485
509
  "row.enable": "启用",
486
510
  "row.delete": "移入回收站",
487
511
  "row.open": "查看 {name} 详情(回车)",
512
+ "row.duplicate": "重名",
513
+ "row.duplicateHint": "同名技能存在多个来源,当前显示为优先级最高的版本",
514
+ "row.copy": "复制",
515
+ "row.copyMention": "复制 $mention",
516
+ "row.copied": "已复制",
517
+ "row.copyPath": "复制路径",
518
+ "detail.copyMention": "复制 $mention",
519
+ "detail.copyPath": "复制路径",
520
+ "detail.copied": "已复制",
521
+ "diag.fix": "修复",
522
+ "diag.fixing": "修复中…",
523
+ "diag.fixed": "已修复",
524
+ "diag.fixConfirm": "用自动修复重写以下文件的 frontmatter?建议先备份:{path}",
525
+ "filter.invocation": "调用",
526
+ "filter.invocationAll": "全部调用",
527
+ "filter.modelOnly": "模型可调",
528
+ "filter.userOnly": "用户可调",
529
+ "filter.showing": "显示 {shown}/{total}(已启用){filtered}",
530
+ "filter.filteredSuffix": " · 已筛选",
531
+ "filter.shortened": "短描述 {count} 条,悬浮看全文",
488
532
  "delete.confirmTitle": "移入回收站?",
489
533
  "delete.confirmText": "确定把「{name}」移入回收站吗?可随时恢复。",
490
534
  "delete.confirm": "移入回收站",
@@ -632,12 +676,22 @@ window.__ModuleLoader__.load({
632
676
  "market.branchTitle": "Choose a branch",
633
677
  "market.branchHint": "This repo has no releases. Pick a branch to track (main by default):",
634
678
  "market.branchConfirm": "Confirm",
679
+ "market.versionTitle": "Switch tracked version",
680
+ "market.versionText": "Pick the version for {repo} to track; rescan and sync installed skills afterwards",
681
+ "market.versionCurrent": "current {ref}",
682
+ "market.versionReleases": "Releases",
683
+ "market.versionBranches": "Branches",
684
+ "market.versionCustom": "Custom (tag / branch / commit)",
685
+ "market.versionConfirm": "Switch to {ref}",
686
+ "market.versionHint": "Click to switch the tracked version",
687
+ "market.unpinned": "unpinned",
635
688
  "market.syncTitle": "Update local skills",
636
689
  "market.syncText": "Market source aligned to {ref}. Update these local skills?",
637
690
  "market.syncNone": "No local skills tracked from this repo yet.",
638
691
  "market.syncCancel": "Skip",
639
692
  "market.removeHint": "Removing a source does not affect installed skills.",
640
693
  "repo.ready": "{count} importable skills found",
694
+ "repo.truncated": "⚠ Repo too large, only partial skills shown (GitHub truncated)",
641
695
  "repo.empty": "No importable skills found.",
642
696
  "repo.selectAll": "Select all",
643
697
  "repo.clearAll": "Clear",
@@ -710,6 +764,26 @@ window.__ModuleLoader__.load({
710
764
  "row.enable": "Enable",
711
765
  "row.delete": "Move to trash",
712
766
  "row.open": "View {name} details (Enter)",
767
+ "row.duplicate": "duplicate",
768
+ "row.duplicateHint": "Multiple sources provide this name; the highest-priority one is shown",
769
+ "row.copy": "Copy",
770
+ "row.copyMention": "Copy $mention",
771
+ "row.copied": "Copied",
772
+ "row.copyPath": "Copy path",
773
+ "detail.copyMention": "Copy $mention",
774
+ "detail.copyPath": "Copy path",
775
+ "detail.copied": "Copied",
776
+ "diag.fix": "Fix",
777
+ "diag.fixing": "Fixing…",
778
+ "diag.fixed": "Fixed",
779
+ "diag.fixConfirm": "Rewrite the frontmatter of this file with the auto-fix? Back it up first: {path}",
780
+ "filter.invocation": "Invocation",
781
+ "filter.invocationAll": "All",
782
+ "filter.modelOnly": "Model",
783
+ "filter.userOnly": "User",
784
+ "filter.showing": "Showing {shown}/{total} (enabled){filtered}",
785
+ "filter.filteredSuffix": " · filtered",
786
+ "filter.shortened": "{count} short descriptions, hover for full text",
713
787
  "delete.confirmTitle": "Move to trash?",
714
788
  "delete.confirmText": "Move \"{name}\" to trash? It can be restored anytime.",
715
789
  "delete.confirm": "Move to trash",
@@ -846,6 +920,367 @@ window.__ModuleLoader__.load({
846
920
  };
847
921
  }
848
922
  //#endregion
923
+ //#region src/client/grouping.ts
924
+ /**
925
+ * Derive a group switch view from its member names and the set of currently
926
+ * enabled skill names (catalog.skills). Members not in the enabled set count
927
+ * as disabled (catalog.disabled or absent read-only rows).
928
+ */
929
+ function groupSwitchView(members, enabledNames) {
930
+ const enabled = [];
931
+ const disabled = [];
932
+ for (const name of members) if (enabledNames.has(name)) enabled.push(name);
933
+ else disabled.push(name);
934
+ return {
935
+ state: disabled.length === 0 ? "on" : enabled.length === 0 ? "off" : "mixed",
936
+ enabled,
937
+ disabled
938
+ };
939
+ }
940
+ /** Names of every group a skill belongs to (tags + collections). */
941
+ function groupNamesOf(name, tags, collections) {
942
+ const names = [];
943
+ for (const tag of tags) if (tag.skillNames.includes(name)) names.push(tag.name);
944
+ for (const collection of collections) if (collection.skillNames.includes(name)) names.push(collection.name);
945
+ return names;
946
+ }
947
+ /**
948
+ * Members of a group that are currently enabled AND also belong to at least
949
+ * one other group — the set a "close" action must ask about.
950
+ */
951
+ function conflictsOnClose(members, enabledNames, otherGroups) {
952
+ return members.filter((name) => {
953
+ if (!enabledNames.has(name)) return false;
954
+ return otherGroups.some((group) => group.members.includes(name));
955
+ });
956
+ }
957
+ /** Origin-repo filter value: skills with no source record (private skills). */
958
+ const PRIVATE_SOURCE = "private";
959
+ /**
960
+ * Apply the origin filter ('all' or a specific origin repo; skills without a
961
+ * source record count as PRIVATE_SOURCE). The origins map is the store's
962
+ * skillName → repo derivation, so filtering follows the tracked source
963
+ * records instead of the filesystem root a skill happens to live under.
964
+ * 项目级技能(有 workspace 归属)永远不算「个人」。
965
+ */
966
+ function filterBySource(skills, source, origins) {
967
+ if (source === "all") return [...skills];
968
+ return skills.filter((skill) => {
969
+ if (isProjectSource(skill.source)) return false;
970
+ return (origins[skill.name] ?? "private") === source;
971
+ });
972
+ }
973
+ /**
974
+ * Sort a skill list in place-safe copy order: name ascending, added
975
+ * descending (newest first, unknown addedAt last), or uses descending
976
+ * (most-called first). Unknown values always trail.
977
+ */
978
+ function sortSkills(skills, key, getUses) {
979
+ const list = [...skills];
980
+ if (key === "name") list.sort((a, b) => a.name.localeCompare(b.name));
981
+ else if (key === "added") list.sort((a, b) => (b.addedAt ?? -Infinity) - (a.addedAt ?? -Infinity));
982
+ else if (key === "uses") list.sort((a, b) => (getUses?.(b.name) ?? 0) - (getUses?.(a.name) ?? 0));
983
+ return list;
984
+ }
985
+ /** Format an epoch-ms timestamp as a short relative time bucket. */
986
+ function formatRelativeTime(ms, now = Date.now()) {
987
+ const diff = Math.max(0, now - ms);
988
+ const minutes = Math.floor(diff / 6e4);
989
+ if (minutes < 1) return { key: "time.justNow" };
990
+ if (minutes < 60) return {
991
+ key: "time.minutesAgo",
992
+ value: minutes
993
+ };
994
+ const hours = Math.floor(minutes / 60);
995
+ if (hours < 24) return {
996
+ key: "time.hoursAgo",
997
+ value: hours
998
+ };
999
+ const days = Math.floor(hours / 24);
1000
+ if (days < 7) return {
1001
+ key: "time.daysAgo",
1002
+ value: days
1003
+ };
1004
+ return {
1005
+ key: "time.weeksAgo",
1006
+ value: Math.floor(days / 7)
1007
+ };
1008
+ }
1009
+ //#endregion
1010
+ //#region src/client/helpers.ts
1011
+ /**
1012
+ * Shared panel helpers: the active-dictionary pick (document-language
1013
+ * based, family precedent) plus a small error-message extractor.
1014
+ */
1015
+ /** Active dictionary, picked by the document language at call time. */
1016
+ function dictionary() {
1017
+ return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
1018
+ }
1019
+ /** Translate a key with optional {name} template params (current language). */
1020
+ function tt(key, values) {
1021
+ return t(dictionary(), key, values);
1022
+ }
1023
+ /** Human-readable error text from an unknown thrown value. */
1024
+ function errorMessage(error) {
1025
+ if (error instanceof Error) return error.message;
1026
+ return String(error);
1027
+ }
1028
+ /**
1029
+ * Whether a displayName is worth showing beside the kebab-case name.
1030
+ * Pure case/punctuation variants (e.g. "Code Review" vs "code-review") are redundant.
1031
+ */
1032
+ function isDisplayNameDistinct(name, displayName) {
1033
+ if (displayName === void 0) return false;
1034
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "");
1035
+ return normalize(displayName) !== normalize(name);
1036
+ }
1037
+ /**
1038
+ * Copy text to the clipboard. Uses the async API with a positioned-textarea
1039
+ * fallback for insecure contexts. Resolves true only when the copy succeeded.
1040
+ */
1041
+ async function copyTextToClipboard(text) {
1042
+ if (navigator.clipboard?.writeText !== void 0) try {
1043
+ await navigator.clipboard.writeText(text);
1044
+ return true;
1045
+ } catch {}
1046
+ try {
1047
+ const ta = document.createElement("textarea");
1048
+ ta.value = text;
1049
+ ta.setAttribute("readonly", "");
1050
+ ta.style.position = "fixed";
1051
+ ta.style.top = "0";
1052
+ ta.style.opacity = "0";
1053
+ document.body.appendChild(ta);
1054
+ ta.select();
1055
+ const ok = document.execCommand("copy");
1056
+ ta.remove();
1057
+ return ok;
1058
+ } catch {
1059
+ return false;
1060
+ }
1061
+ }
1062
+ //#endregion
1063
+ //#region src/client/panel/format.ts
1064
+ /** Model-invocable dot color default. Single source for the TS side; the
1065
+ * panel's CSS mirrors it via --hub-model (panel.module.css). */
1066
+ const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
1067
+ /** User-invocable dot color default. Single source for the TS side; the
1068
+ * panel's CSS mirrors it via --hub-user (panel.module.css). */
1069
+ const DEFAULT_DOT_USER_COLOR = "#3fb950";
1070
+ /** Dot inline style from the user-chosen color (undefined keeps the CSS default). */
1071
+ function dotStyle(color) {
1072
+ if (color === void 0) return void 0;
1073
+ return {
1074
+ background: color,
1075
+ borderColor: color
1076
+ };
1077
+ }
1078
+ /** Localized relative-time text for a Unix-ms timestamp. */
1079
+ function relativeTimeText(ms) {
1080
+ const rt = formatRelativeTime(ms);
1081
+ return tt(rt.key, rt.value !== void 0 ? { value: rt.value } : void 0);
1082
+ }
1083
+ /** Localized absolute time for the detail meta (e.g. "2026/8/16 11:00:00"). */
1084
+ function formatDateTime(ms) {
1085
+ return new Date(ms).toLocaleString();
1086
+ }
1087
+ /** Short form of a commit SHA for display. */
1088
+ function shortSha(sha) {
1089
+ return sha.length > 7 ? sha.slice(0, 7) : sha;
1090
+ }
1091
+ //#endregion
1092
+ //#region src/client/slash-dots.tsx
1093
+ /** The core plugin's skill source identity on the '/' trigger. */
1094
+ const SKILL_SOURCE = {
1095
+ trigger: "/",
1096
+ name: "skill"
1097
+ };
1098
+ /** How long one catalog-derived modelInvocable map stays hot before refresh. */
1099
+ const MODEL_TTL_MS = 6e4;
1100
+ /**
1101
+ * Find the core `/skill` source through the runtime registry, or undefined.
1102
+ * The lookup never throws: a missing/reshaped registry just means no dots. Exported
1103
+ * for unit tests; the apply path only calls it indirectly through setupSkillSlashDots.
1104
+ * @param service - the ctx.inputTriggers service face.
1105
+ * @returns the registered skill source, or undefined.
1106
+ */
1107
+ function findSkillSource(service) {
1108
+ const live = service.live;
1109
+ if (live?.sources === void 0) return void 0;
1110
+ return live.sources.find((source) => source.trigger === SKILL_SOURCE.trigger && source.name === SKILL_SOURCE.name);
1111
+ }
1112
+ /** One catalog-derived name → modelInvocable snapshot with a load timestamp. */
1113
+ let modelCache;
1114
+ /**
1115
+ * Clear the modelInvocable cache. Called on connection/reset so a fresh
1116
+ * catalog wins after reconnect; exported for deterministic unit tests.
1117
+ */
1118
+ function resetModelCache() {
1119
+ modelCache = void 0;
1120
+ }
1121
+ /**
1122
+ * Resolve name → modelInvocable from the hub's catalog, cached for
1123
+ * MODEL_TTL_MS. A failed load caches the failure briefly so a downed route
1124
+ * doesn't hammer the host on every keystroke; the returned map is empty then
1125
+ * (callers fall back to the model dot for unknown names).
1126
+ * @param api - the hub browser API.
1127
+ * @returns name → whether the model may call the skill.
1128
+ */
1129
+ async function modelInvocableMap(api) {
1130
+ const now = Date.now();
1131
+ const cached = modelCache;
1132
+ if (cached !== void 0 && now - cached.at < MODEL_TTL_MS) return "failed" in cached ? /* @__PURE__ */ new Map() : cached.map;
1133
+ try {
1134
+ const catalog = await api.catalog();
1135
+ const map = new Map(catalog.skills.map((skill) => [skill.name, skill.invocation.modelInvocable]));
1136
+ modelCache = {
1137
+ at: now,
1138
+ map
1139
+ };
1140
+ return map;
1141
+ } catch (error) {
1142
+ console.error("[dsh-skill-hub] slash dot color lookup failed:", error);
1143
+ modelCache = {
1144
+ at: now,
1145
+ failed: true
1146
+ };
1147
+ return /* @__PURE__ */ new Map();
1148
+ }
1149
+ }
1150
+ /**
1151
+ * One menu-row dot element. Inline span so it needs no CSS module; the
1152
+ * candidate menu centers it inside its 16×16 leading icon slot.
1153
+ * @param color - the dot's background color.
1154
+ * @returns a React node (memory-only, never crosses the Host boundary).
1155
+ */
1156
+ function dotIcon(color) {
1157
+ return (0, react.createElement)("span", {
1158
+ "aria-hidden": true,
1159
+ style: {
1160
+ display: "inline-block",
1161
+ width: 6,
1162
+ height: 6,
1163
+ borderRadius: 3,
1164
+ background: color,
1165
+ flex: "none"
1166
+ }
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Wrap the core skill source so every menu row carries the invocation dot.
1171
+ * Exported for unit tests; production wiring goes through setupSkillSlashDots.
1172
+ * @param source - the registered `/skill` source.
1173
+ * @param api - hub browser API for the modelInvocable lookup.
1174
+ * @param scope - hub settings scope for the dot colors.
1175
+ * @returns a disposer restoring the original candidates.
1176
+ */
1177
+ /**
1178
+ * DOM 兜底:在 alpha.2 新版 MenuView(icon 仅枚举)下通过直接操作
1179
+ * 已渲染的 `[role="option"]` 列表注入彩色点,绕过 `icon` 限制。
1180
+ * 旧版仍走 `icon` 注入,此处仅为新版。
1181
+ */
1182
+ function injectDotsViaDOM(modelByName, modelColor, userColor) {
1183
+ if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return;
1184
+ const run = () => {
1185
+ const listbox = document.querySelector("[class*=\"_3e4SsG_menu\"]")?.querySelector("[role=\"listbox\"]") ?? document.querySelector("[role=\"listbox\"]");
1186
+ if (listbox === null) return;
1187
+ const options = listbox.querySelectorAll("[role=\"option\"]");
1188
+ for (const opt of options) {
1189
+ if (opt.querySelector("[data-skill-dot]") !== null) continue;
1190
+ const nameEl = opt.querySelector("[class*=\"itemName\"]");
1191
+ if (nameEl === null) continue;
1192
+ const name = (nameEl.textContent ?? "").trim();
1193
+ if (name.length === 0) continue;
1194
+ if (!modelByName.has(name)) continue;
1195
+ const color = modelByName.get(name) ?? true ? modelColor : userColor;
1196
+ const wrapper = document.createElement("span");
1197
+ wrapper.setAttribute("data-skill-dot", "");
1198
+ wrapper.setAttribute("aria-hidden", "true");
1199
+ wrapper.style.width = "16px";
1200
+ wrapper.style.height = "16px";
1201
+ wrapper.style.display = "inline-flex";
1202
+ wrapper.style.justifyContent = "center";
1203
+ wrapper.style.alignItems = "center";
1204
+ wrapper.style.flex = "none";
1205
+ const dot = document.createElement("span");
1206
+ dot.style.display = "inline-block";
1207
+ dot.style.width = "6px";
1208
+ dot.style.height = "6px";
1209
+ dot.style.borderRadius = "3px";
1210
+ dot.style.background = color;
1211
+ dot.style.flex = "none";
1212
+ wrapper.appendChild(dot);
1213
+ nameEl.parentElement?.insertBefore(wrapper, nameEl);
1214
+ }
1215
+ };
1216
+ requestAnimationFrame(() => setTimeout(run, 0));
1217
+ }
1218
+ function wrapSkillSource(source, api, scope) {
1219
+ const original = source.candidates;
1220
+ source.candidates = async (session, req) => {
1221
+ const items = await original(session, req);
1222
+ if (req.signal.aborted) return items;
1223
+ const isNewHost = req !== null && typeof req === "object" && "drilled" in req;
1224
+ const modelByName = await modelInvocableMap(api);
1225
+ if (req.signal.aborted) return items;
1226
+ const snapshot = scope.getSnapshot();
1227
+ const modelColor = snapshot.value?.dotModelColor ?? "#2f81f7";
1228
+ const userColor = snapshot.value?.dotUserColor ?? "#3fb950";
1229
+ if (isNewHost) {
1230
+ injectDotsViaDOM(modelByName, modelColor, userColor);
1231
+ return items;
1232
+ }
1233
+ return items.map((item) => ({
1234
+ ...item,
1235
+ icon: dotIcon(modelByName.get(item.name) ?? true ? modelColor : userColor)
1236
+ }));
1237
+ };
1238
+ return () => {
1239
+ source.candidates = original;
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Mount the slash-menu dots on the registered `/skill` source. Idempotent and
1244
+ * defensive: if the core source isn't registered yet (or the registry shape
1245
+ * changes), it retries briefly and then gives up silently — the chat keeps
1246
+ * working, it simply shows no dots. The returned disposer restores the
1247
+ * original candidates and clears the model cache.
1248
+ * @param ctx - the client root context (inputTriggers + events).
1249
+ * @param api - hub browser API.
1250
+ * @param scope - hub settings scope for dot colors.
1251
+ * @returns a cleanup function for `ctx.effect`.
1252
+ */
1253
+ function setupSkillSlashDots(ctx, api, scope) {
1254
+ const inputTriggers = ctx.get("inputTriggers");
1255
+ if (inputTriggers === void 0) return () => {};
1256
+ let disposed = false;
1257
+ let restore;
1258
+ let attempts = 0;
1259
+ const attempt = () => {
1260
+ if (disposed) return;
1261
+ const source = findSkillSource(inputTriggers);
1262
+ if (source === void 0) {
1263
+ if (attempts < 10) {
1264
+ attempts += 1;
1265
+ setTimeout(attempt, 100);
1266
+ }
1267
+ return;
1268
+ }
1269
+ restore = wrapSkillSource(source, api, scope);
1270
+ };
1271
+ attempt();
1272
+ const clearCache = () => {
1273
+ resetModelCache();
1274
+ };
1275
+ const offReset = ctx.on("connection/reset", clearCache);
1276
+ return () => {
1277
+ disposed = true;
1278
+ offReset();
1279
+ restore?.();
1280
+ restore = void 0;
1281
+ };
1282
+ }
1283
+ //#endregion
849
1284
  //#region src/client/icons.tsx
850
1285
  /** ic_ds_chevron_down_outline_14 */
851
1286
  function IconChevronDownOutline14({ size = 14, className }) {
@@ -2078,7 +2513,7 @@ window.__ModuleLoader__.load({
2078
2513
  }
2079
2514
  async store(field, value) {
2080
2515
  await this.scope.set(field, value);
2081
- return this.userLayer()?.[field] === value;
2516
+ return this.userRecord()?.[field] === value;
2082
2517
  }
2083
2518
  userRecord() {
2084
2519
  const user = this.scope.getSnapshot().user;
@@ -2101,9 +2536,6 @@ window.__ModuleLoader__.load({
2101
2536
  const base = this.scope.getSnapshot().base;
2102
2537
  return typeof base === "object" && base !== null ? base[field] : void 0;
2103
2538
  }
2104
- userLayer() {
2105
- return this.userRecord();
2106
- }
2107
2539
  stored(field) {
2108
2540
  const user = this.userRecord();
2109
2541
  return user !== void 0 && Object.hasOwn(user, field);
@@ -2113,141 +2545,6 @@ window.__ModuleLoader__.load({
2113
2545
  }
2114
2546
  };
2115
2547
  //#endregion
2116
- //#region src/client/grouping.ts
2117
- /**
2118
- * Derive a group switch view from its member names and the set of currently
2119
- * enabled skill names (catalog.skills). Members not in the enabled set count
2120
- * as disabled (catalog.disabled or absent read-only rows).
2121
- */
2122
- function groupSwitchView(members, enabledNames) {
2123
- const enabled = [];
2124
- const disabled = [];
2125
- for (const name of members) if (enabledNames.has(name)) enabled.push(name);
2126
- else disabled.push(name);
2127
- return {
2128
- state: disabled.length === 0 ? "on" : enabled.length === 0 ? "off" : "mixed",
2129
- enabled,
2130
- disabled
2131
- };
2132
- }
2133
- /** Names of every group a skill belongs to (tags + collections). */
2134
- function groupNamesOf(name, tags, collections) {
2135
- const names = [];
2136
- for (const tag of tags) if (tag.skillNames.includes(name)) names.push(tag.name);
2137
- for (const collection of collections) if (collection.skillNames.includes(name)) names.push(collection.name);
2138
- return names;
2139
- }
2140
- /**
2141
- * Members of a group that are currently enabled AND also belong to at least
2142
- * one other group — the set a "close" action must ask about.
2143
- */
2144
- function conflictsOnClose(members, enabledNames, otherGroups) {
2145
- return members.filter((name) => {
2146
- if (!enabledNames.has(name)) return false;
2147
- return otherGroups.some((group) => group.members.includes(name));
2148
- });
2149
- }
2150
- /** Origin-repo filter value: skills with no source record (private skills). */
2151
- const PRIVATE_SOURCE = "private";
2152
- /**
2153
- * Apply the origin filter ('all' or a specific origin repo; skills without a
2154
- * source record count as PRIVATE_SOURCE). The origins map is the store's
2155
- * skillName → repo derivation, so filtering follows the tracked source
2156
- * records instead of the filesystem root a skill happens to live under.
2157
- * 项目级技能(有 workspace 归属)永远不算「个人」。
2158
- */
2159
- function filterBySource(skills, source, origins) {
2160
- if (source === "all") return [...skills];
2161
- return skills.filter((skill) => {
2162
- if (isProjectSource(skill.source)) return false;
2163
- return (origins[skill.name] ?? "private") === source;
2164
- });
2165
- }
2166
- /**
2167
- * Sort a skill list in place-safe copy order: name ascending, added
2168
- * descending (newest first, unknown addedAt last), or uses descending
2169
- * (most-called first). Unknown values always trail.
2170
- */
2171
- function sortSkills(skills, key, getUses) {
2172
- const list = [...skills];
2173
- if (key === "name") list.sort((a, b) => a.name.localeCompare(b.name));
2174
- else if (key === "added") list.sort((a, b) => (b.addedAt ?? -Infinity) - (a.addedAt ?? -Infinity));
2175
- else if (key === "uses") list.sort((a, b) => (getUses?.(b.name) ?? 0) - (getUses?.(a.name) ?? 0));
2176
- return list;
2177
- }
2178
- /** Format an epoch-ms timestamp as a short relative time bucket. */
2179
- function formatRelativeTime(ms, now = Date.now()) {
2180
- const diff = Math.max(0, now - ms);
2181
- const minutes = Math.floor(diff / 6e4);
2182
- if (minutes < 1) return { key: "time.justNow" };
2183
- if (minutes < 60) return {
2184
- key: "time.minutesAgo",
2185
- value: minutes
2186
- };
2187
- const hours = Math.floor(minutes / 60);
2188
- if (hours < 24) return {
2189
- key: "time.hoursAgo",
2190
- value: hours
2191
- };
2192
- const days = Math.floor(hours / 24);
2193
- if (days < 7) return {
2194
- key: "time.daysAgo",
2195
- value: days
2196
- };
2197
- return {
2198
- key: "time.weeksAgo",
2199
- value: Math.floor(days / 7)
2200
- };
2201
- }
2202
- //#endregion
2203
- //#region src/client/helpers.ts
2204
- /**
2205
- * Shared panel helpers: the active-dictionary pick (document-language
2206
- * based, family precedent) plus a small error-message extractor.
2207
- */
2208
- /** Active dictionary, picked by the document language at call time. */
2209
- function dictionary() {
2210
- return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
2211
- }
2212
- /** Translate a key with optional {name} template params (current language). */
2213
- function tt(key, values) {
2214
- return t(dictionary(), key, values);
2215
- }
2216
- /** Human-readable error text from an unknown thrown value. */
2217
- function errorMessage(error) {
2218
- if (error instanceof Error) return error.message;
2219
- return String(error);
2220
- }
2221
- //#endregion
2222
- //#region src/client/panel/format.ts
2223
- /** Model-invocable dot color default. Single source for the TS side; the
2224
- * panel's CSS mirrors it via --hub-model (panel.module.css). */
2225
- const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
2226
- /** User-invocable dot color default. Single source for the TS side; the
2227
- * panel's CSS mirrors it via --hub-user (panel.module.css). */
2228
- const DEFAULT_DOT_USER_COLOR = "#3fb950";
2229
- /** Dot inline style from the user-chosen color (undefined keeps the CSS default). */
2230
- function dotStyle(color) {
2231
- if (color === void 0) return void 0;
2232
- return {
2233
- background: color,
2234
- borderColor: color
2235
- };
2236
- }
2237
- /** Localized relative-time text for a Unix-ms timestamp. */
2238
- function relativeTimeText(ms) {
2239
- const rt = formatRelativeTime(ms);
2240
- return tt(rt.key, rt.value !== void 0 ? { value: rt.value } : void 0);
2241
- }
2242
- /** Localized absolute time for the detail meta (e.g. "2026/8/16 11:00:00"). */
2243
- function formatDateTime(ms) {
2244
- return new Date(ms).toLocaleString();
2245
- }
2246
- /** Short form of a commit SHA for display. */
2247
- function shortSha(sha) {
2248
- return sha.length > 7 ? sha.slice(0, 7) : sha;
2249
- }
2250
- //#endregion
2251
2548
  //#region src/client/SkillHubSettingsCard.tsx
2252
2549
  /**
2253
2550
  * The dsh-skill-hub plugin settings card: bridges the hub's settings
@@ -2466,7 +2763,7 @@ window.__ModuleLoader__.load({
2466
2763
  }
2467
2764
  //#endregion
2468
2765
  //#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/panel/panel.module.css.mjs
2469
- 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-wrap:wrap;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_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}";
2766
+ 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-wrap:wrap;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_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_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_displayName{opacity:.55;white-space:nowrap;text-overflow:ellipsis;flex-shrink:1;min-width:0;max-width:160px;margin-left:6px;font-size:11px;font-weight:400;overflow:hidden}._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_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_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_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:.28;letter-spacing:0;user-select:none;flex:none;justify-content:center;align-self:center;align-items:center;width:8px;height:16px;padding:0 1px;font-size:10px;display:inline-flex;overflow:hidden}._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}";
2470
2767
  const tagId = "dsh-skill-hub/panel.module.css";
2471
2768
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2472
2769
  const tag = document.createElement("style");
@@ -2477,18 +2774,13 @@ window.__ModuleLoader__.load({
2477
2774
  }
2478
2775
  var panel_module_css_default = {
2479
2776
  "actions": "_6VtqdG_actions",
2480
- "actionsBottom": "_6VtqdG_actionsBottom",
2481
2777
  "actionsPadded": "_6VtqdG_actionsPadded",
2482
2778
  "actionsTop": "_6VtqdG_actionsTop",
2483
2779
  "back": "_6VtqdG_back",
2484
2780
  "badge": "_6VtqdG_badge",
2485
2781
  "badgeCount": "_6VtqdG_badgeCount",
2486
- "badgeDisabled": "_6VtqdG_badgeDisabled",
2487
- "badgeModel": "_6VtqdG_badgeModel",
2488
2782
  "badgeReadonly": "_6VtqdG_badgeReadonly",
2489
2783
  "badgeSource": "_6VtqdG_badgeSource",
2490
- "badgeUser": "_6VtqdG_badgeUser",
2491
- "badgeUses": "_6VtqdG_badgeUses",
2492
2784
  "badges": "_6VtqdG_badges",
2493
2785
  "button": "_6VtqdG_button",
2494
2786
  "buttons": "_6VtqdG_buttons",
@@ -2512,6 +2804,7 @@ window.__ModuleLoader__.load({
2512
2804
  "dialogText": "_6VtqdG_dialogText",
2513
2805
  "dialogTitle": "_6VtqdG_dialogTitle",
2514
2806
  "disclosure": "_6VtqdG_disclosure",
2807
+ "displayName": "_6VtqdG_displayName",
2515
2808
  "dot": "_6VtqdG_dot",
2516
2809
  "dotModel": "_6VtqdG_dotModel",
2517
2810
  "dotUser": "_6VtqdG_dotUser",
@@ -2555,7 +2848,6 @@ window.__ModuleLoader__.load({
2555
2848
  "row": "_6VtqdG_row",
2556
2849
  "rowDesc": "_6VtqdG_rowDesc",
2557
2850
  "rowMain": "_6VtqdG_rowMain",
2558
- "rowMeta": "_6VtqdG_rowMeta",
2559
2851
  "rowMuted": "_6VtqdG_rowMuted",
2560
2852
  "rowName": "_6VtqdG_rowName",
2561
2853
  "rowNameText": "_6VtqdG_rowNameText",
@@ -2653,32 +2945,108 @@ window.__ModuleLoader__.load({
2653
2945
  }
2654
2946
  //#endregion
2655
2947
  //#region src/client/panel/SkillDetailView.tsx
2948
+ /**
2949
+ * Full-page skill detail view: metadata, upstream source card with
2950
+ * check/sync/follow-delete actions, and the raw SKILL.md body. Pure
2951
+ * presentation — every state value and action arrives as a prop, so the
2952
+ * panel stays the single owner of state.
2953
+ */
2656
2954
  function SkillDetailView(props) {
2657
- const { detail, hubConfig, uses, groupsState, sourcesState, sourceCheck, checkingSource, syncingSource, loading, onBack, onCheck, onSync, onFollowDelete } = props;
2955
+ const { detail, hubConfig, uses, groupsState, sourcesState, sourceCheck, checkingSource, syncingSource, loading, disabled, onEnable, onBack, onCheck, onSync, onFollowDelete } = props;
2658
2956
  const detailSource = sourcesState?.sources.find((source) => source.skills.includes(detail.name));
2659
2957
  const detailCheck = detailSource !== void 0 ? sourceCheck[detailSource.repo] : void 0;
2958
+ const [copied, setCopied] = (0, react.useState)(null);
2959
+ const copyTimer = (0, react.useRef)(void 0);
2960
+ (0, react.useEffect)(() => () => {
2961
+ window.clearTimeout(copyTimer.current);
2962
+ }, []);
2963
+ const copyText = (text, key) => {
2964
+ copyTextToClipboard(text).then((ok) => {
2965
+ if (!ok) return;
2966
+ setCopied(key);
2967
+ window.clearTimeout(copyTimer.current);
2968
+ copyTimer.current = window.setTimeout(() => {
2969
+ setCopied(null);
2970
+ }, 1200);
2971
+ });
2972
+ };
2660
2973
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2661
2974
  className: panel_module_css_default.panel,
2662
2975
  children: [
2663
2976
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2664
2977
  className: panel_module_css_default.detailHead,
2665
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2666
- type: "button",
2667
- className: panel_module_css_default.back,
2668
- onClick: onBack,
2669
- children: tt("detail.back")
2670
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2671
- className: panel_module_css_default.detailName,
2672
- children: [detail.name, detail.invocation.modelInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2673
- className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
2674
- style: dotStyle(hubConfig?.dotModelColor),
2675
- title: tt("legend.model")
2676
- }) : detail.invocation.userInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2677
- className: panel_module_css_default.dot + " " + panel_module_css_default.dotUser,
2678
- style: dotStyle(hubConfig?.dotUserColor),
2679
- title: tt("legend.user")
2680
- }) : null]
2681
- })]
2978
+ children: [
2979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2980
+ type: "button",
2981
+ className: panel_module_css_default.back,
2982
+ onClick: onBack,
2983
+ children: tt("detail.back")
2984
+ }),
2985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2986
+ className: panel_module_css_default.detailName,
2987
+ style: {
2988
+ display: "inline-flex",
2989
+ alignItems: "center",
2990
+ gap: 6,
2991
+ flexWrap: "wrap"
2992
+ },
2993
+ children: [
2994
+ detail.name,
2995
+ isDisplayNameDistinct(detail.name, detail.displayName) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2996
+ className: panel_module_css_default.displayName,
2997
+ style: {
2998
+ fontSize: 13,
2999
+ marginLeft: 0
3000
+ },
3001
+ children: detail.displayName
3002
+ }) : null,
3003
+ detail.invocation.modelInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3004
+ className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
3005
+ style: dotStyle(hubConfig?.dotModelColor),
3006
+ title: tt("legend.model")
3007
+ }) : detail.invocation.userInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3008
+ className: panel_module_css_default.dot + " " + panel_module_css_default.dotUser,
3009
+ style: dotStyle(hubConfig?.dotUserColor),
3010
+ title: tt("legend.user")
3011
+ }) : null
3012
+ ]
3013
+ }),
3014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3015
+ className: panel_module_css_default.actions,
3016
+ style: {
3017
+ marginLeft: "auto",
3018
+ gap: 6
3019
+ },
3020
+ children: [
3021
+ disabled === true && onEnable !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3022
+ type: "button",
3023
+ className: panel_module_css_default.opBtn,
3024
+ role: "switch",
3025
+ "aria-checked": false,
3026
+ "aria-label": tt("row.enable"),
3027
+ onClick: onEnable,
3028
+ children: tt("row.enable")
3029
+ }) : null,
3030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3031
+ type: "button",
3032
+ className: panel_module_css_default.opBtn,
3033
+ onClick: () => {
3034
+ copyText("$" + detail.name, "mention");
3035
+ },
3036
+ children: copied === "mention" ? tt("detail.copied") : tt("detail.copyMention")
3037
+ }),
3038
+ detail.path !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3039
+ type: "button",
3040
+ className: panel_module_css_default.opBtn,
3041
+ onClick: () => {
3042
+ const path = detail.path;
3043
+ if (path !== void 0) copyText(path, "path");
3044
+ },
3045
+ children: copied === "path" ? tt("detail.copied") : tt("detail.copyPath")
3046
+ }) : null
3047
+ ]
3048
+ })
3049
+ ]
2682
3050
  }),
2683
3051
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2684
3052
  className: panel_module_css_default.detailMeta,
@@ -2780,34 +3148,30 @@ window.__ModuleLoader__.load({
2780
3148
  }),
2781
3149
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2782
3150
  className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
2783
- children: [
2784
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2785
- type: "button",
2786
- className: panel_module_css_default.opBtn,
2787
- disabled: checkingSource !== null,
2788
- onClick: () => {
2789
- onCheck(detailSource.repo);
2790
- },
2791
- children: checkingSource === detailSource.repo ? tt("source.checking") : tt("source.check")
2792
- }),
2793
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2794
- type: "button",
2795
- className: panel_module_css_default.opBtn,
2796
- disabled: syncingSource !== null,
2797
- onClick: () => {
2798
- onSync(detailSource.repo, [detail.name]);
2799
- },
2800
- children: syncingSource === detailSource.repo ? tt("source.syncing") : tt("source.sync")
2801
- }),
2802
- detailCheck?.deleted.includes(detail.name) === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2803
- type: "button",
2804
- className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
2805
- onClick: () => {
2806
- onFollowDelete(detailSource.repo, [detail.name]);
2807
- },
2808
- children: tt("source.followDelete")
2809
- }) : null
2810
- ]
3151
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3152
+ type: "button",
3153
+ className: panel_module_css_default.opBtn,
3154
+ disabled: checkingSource !== null,
3155
+ onClick: () => {
3156
+ onCheck(detailSource.repo);
3157
+ },
3158
+ children: checkingSource === detailSource.repo ? tt("source.checking") : tt("source.check")
3159
+ }), disabled !== true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3160
+ type: "button",
3161
+ className: panel_module_css_default.opBtn,
3162
+ disabled: syncingSource !== null,
3163
+ onClick: () => {
3164
+ onSync(detailSource.repo, [detail.name]);
3165
+ },
3166
+ children: syncingSource === detailSource.repo ? tt("source.syncing") : tt("source.sync")
3167
+ }), detailCheck?.deleted.includes(detail.name) === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3168
+ type: "button",
3169
+ className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
3170
+ onClick: () => {
3171
+ onFollowDelete(detailSource.repo, [detail.name]);
3172
+ },
3173
+ children: tt("source.followDelete")
3174
+ }) : null] }) : null]
2811
3175
  })
2812
3176
  ]
2813
3177
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -2944,6 +3308,7 @@ window.__ModuleLoader__.load({
2944
3308
  const stat = hub.uses.get(skill.name);
2945
3309
  const count = stat?.count ?? 0;
2946
3310
  const lastUsed = stat?.lastUsed;
3311
+ const isDuplicate = hub.catalog?.duplicateNames?.includes(skill.name) === true;
2947
3312
  /** 单一状态圆点:模型可调 → 蓝;否则用户可调 → 绿。与聊天 / 菜单同规则。 */
2948
3313
  const dot = skill.invocation.modelInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2949
3314
  className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
@@ -2980,11 +3345,21 @@ window.__ModuleLoader__.load({
2980
3345
  className: panel_module_css_default.rowNameText,
2981
3346
  children: skill.name
2982
3347
  }),
3348
+ isDisplayNameDistinct(skill.name, skill.displayName) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3349
+ className: panel_module_css_default.displayName,
3350
+ title: skill.displayName,
3351
+ children: skill.displayName
3352
+ }) : null,
2983
3353
  count > 0 && hub.hubConfig?.showUseCount !== false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2984
3354
  className: panel_module_css_default.useCount,
2985
3355
  children: count
2986
3356
  }) : null,
2987
3357
  dot,
3358
+ isDuplicate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3359
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
3360
+ title: tt("row.duplicateHint"),
3361
+ children: tt("row.duplicate")
3362
+ }) : null,
2988
3363
  lastUsed !== void 0 && hub.hubConfig?.showUseTime !== false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2989
3364
  className: panel_module_css_default.useTime,
2990
3365
  children: relativeTimeText(lastUsed)
@@ -2992,7 +3367,8 @@ window.__ModuleLoader__.load({
2992
3367
  ]
2993
3368
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2994
3369
  className: panel_module_css_default.rowDesc,
2995
- children: skill.description
3370
+ title: skill.description,
3371
+ children: skill.shortDescription ?? skill.description
2996
3372
  })]
2997
3373
  }), skill.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2998
3374
  type: "button",
@@ -3030,17 +3406,36 @@ window.__ModuleLoader__.load({
3030
3406
  return tt("badge.source." + root);
3031
3407
  }
3032
3408
  function DisabledRow(props) {
3033
- const { record, busy, onEnable } = props;
3409
+ const { record, busy, onEnable, duplicate, onOpen } = props;
3410
+ /** 键盘打开详情:Enter 或空格。仅当焦点在行本身(而非行内开关)时生效。 */
3411
+ const onKeyDown = (event) => {
3412
+ if (onOpen === void 0 || event.target !== event.currentTarget) return;
3413
+ if (event.key === "Enter" || event.key === " ") {
3414
+ event.preventDefault();
3415
+ onOpen();
3416
+ }
3417
+ };
3034
3418
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3035
- className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
3419
+ className: panel_module_css_default.row + (onOpen === void 0 ? " " + panel_module_css_default.rowStatic : ""),
3420
+ role: onOpen === void 0 ? void 0 : "button",
3421
+ tabIndex: onOpen === void 0 ? void 0 : 0,
3422
+ "aria-label": onOpen === void 0 ? void 0 : tt("row.open", { name: record.name }),
3423
+ onClick: onOpen,
3424
+ onKeyDown,
3036
3425
  children: [
3037
3426
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3038
3427
  className: panel_module_css_default.rowMain,
3039
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3428
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3040
3429
  className: panel_module_css_default.rowName,
3041
- children: record.name
3430
+ children: [record.name, duplicate === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3431
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
3432
+ style: { marginLeft: 6 },
3433
+ title: tt("row.duplicateHint"),
3434
+ children: tt("row.duplicate")
3435
+ }) : null]
3042
3436
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3043
3437
  className: panel_module_css_default.rowDesc,
3438
+ title: record.description,
3044
3439
  children: record.description
3045
3440
  })]
3046
3441
  }),
@@ -3058,7 +3453,10 @@ window.__ModuleLoader__.load({
3058
3453
  "aria-label": tt("row.enable"),
3059
3454
  className: panel_module_css_default.switch,
3060
3455
  disabled: busy,
3061
- onClick: onEnable,
3456
+ onClick: (event) => {
3457
+ event.stopPropagation();
3458
+ onEnable();
3459
+ },
3062
3460
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
3063
3461
  })
3064
3462
  ]
@@ -3415,8 +3813,12 @@ window.__ModuleLoader__.load({
3415
3813
  }, skill.name)), disabledMembers.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
3416
3814
  record,
3417
3815
  busy: busyNames.has(record.name),
3816
+ duplicate: catalog?.duplicateNames?.includes(record.name) === true,
3418
3817
  onEnable: () => {
3419
3818
  enableDisabled(record);
3819
+ },
3820
+ onOpen: () => {
3821
+ hub.openDetail(record.name);
3420
3822
  }
3421
3823
  }, record.name))] }) : null]
3422
3824
  }, "col:" + collection.name);
@@ -3502,8 +3904,12 @@ window.__ModuleLoader__.load({
3502
3904
  }, skill.name)), personalDisabled.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
3503
3905
  record,
3504
3906
  busy: busyNames.has(record.name),
3907
+ duplicate: catalog?.duplicateNames?.includes(record.name) === true,
3505
3908
  onEnable: () => {
3506
3909
  enableDisabled(record);
3910
+ },
3911
+ onOpen: () => {
3912
+ hub.openDetail(record.name);
3507
3913
  }
3508
3914
  }, record.name))] }) : null]
3509
3915
  }, "uncategorized-source");
@@ -3657,8 +4063,12 @@ window.__ModuleLoader__.load({
3657
4063
  }, skill.name)), disabledMembers.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
3658
4064
  record,
3659
4065
  busy: busyNames.has(record.name),
4066
+ duplicate: hub.catalog?.duplicateNames?.includes(record.name) === true,
3660
4067
  onEnable: () => {
3661
4068
  enableDisabled(record);
4069
+ },
4070
+ onOpen: () => {
4071
+ hub.openDetail(record.name);
3662
4072
  }
3663
4073
  }, record.name))] }) : null]
3664
4074
  }, "tag:" + tag.id);
@@ -3803,23 +4213,106 @@ window.__ModuleLoader__.load({
3803
4213
  children: [
3804
4214
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3805
4215
  className: panel_module_css_default.dialogTitle,
3806
- children: tt("market.branchTitle")
4216
+ children: tt("market.branchTitle")
4217
+ }),
4218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4219
+ className: panel_module_css_default.dialogText,
4220
+ children: tt("market.branchHint")
4221
+ }),
4222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4223
+ className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4224
+ value: choice.selected,
4225
+ onChange: (event) => {
4226
+ onSelect(event.target.value);
4227
+ },
4228
+ children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4229
+ value: branch,
4230
+ children: branch
4231
+ }, branch))
4232
+ }),
4233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4234
+ className: panel_module_css_default.dialogActions,
4235
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4236
+ type: "button",
4237
+ className: panel_module_css_default.button,
4238
+ onClick: onCancel,
4239
+ children: tt("form.cancel")
4240
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4241
+ type: "button",
4242
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4243
+ disabled: busy,
4244
+ onClick: onConfirm,
4245
+ children: tt("market.branchConfirm")
4246
+ })]
4247
+ })
4248
+ ]
4249
+ });
4250
+ }
4251
+ /** Version picker: switch a market source's tracked ref (release / branch / custom). */
4252
+ function VersionChoiceDialog(props) {
4253
+ const { choice, busy, onSelect, onCustom, onCancel, onConfirm } = props;
4254
+ const effective = choice.custom.trim() !== "" ? choice.custom.trim() : choice.selected;
4255
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
4256
+ onClose: onCancel,
4257
+ children: [
4258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4259
+ className: panel_module_css_default.dialogTitle,
4260
+ children: tt("market.versionTitle")
3807
4261
  }),
3808
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3809
4263
  className: panel_module_css_default.dialogText,
3810
- children: tt("market.branchHint")
3811
- }),
3812
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3813
- className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
3814
- value: choice.selected,
3815
- onChange: (event) => {
3816
- onSelect(event.target.value);
3817
- },
3818
- children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3819
- value: branch,
3820
- children: branch
3821
- }, branch))
4264
+ children: [tt("market.versionText", { repo: choice.repo }), choice.current !== void 0 ? ` (${tt("market.versionCurrent", { ref: choice.current })})` : ""]
3822
4265
  }),
4266
+ choice.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4267
+ className: panel_module_css_default.dialogText,
4268
+ children: tt("market.scanning")
4269
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4270
+ choice.releases.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4271
+ className: panel_module_css_default.dialogText,
4272
+ style: { marginBottom: 4 },
4273
+ children: tt("market.versionReleases")
4274
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4275
+ className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4276
+ value: choice.releases.includes(choice.selected) ? choice.selected : choice.releases[0],
4277
+ onChange: (event) => {
4278
+ onSelect(event.target.value);
4279
+ onCustom("");
4280
+ },
4281
+ children: choice.releases.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4282
+ value: tag,
4283
+ children: tag
4284
+ }, tag))
4285
+ })] }) : null,
4286
+ choice.branches.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4287
+ className: panel_module_css_default.dialogText,
4288
+ style: { marginBottom: 4 },
4289
+ children: tt("market.versionBranches")
4290
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4291
+ className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4292
+ value: choice.branches.includes(choice.selected) ? choice.selected : choice.branches[0],
4293
+ onChange: (event) => {
4294
+ onSelect(event.target.value);
4295
+ onCustom("");
4296
+ },
4297
+ children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4298
+ value: branch,
4299
+ children: branch
4300
+ }, branch))
4301
+ })] }) : null,
4302
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4303
+ className: panel_module_css_default.dialogText,
4304
+ style: { marginBottom: 4 },
4305
+ children: tt("market.versionCustom")
4306
+ }),
4307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4308
+ className: panel_module_css_default.input + " " + panel_module_css_default.dialogSelect,
4309
+ value: choice.custom,
4310
+ onChange: (event) => {
4311
+ onCustom(event.target.value);
4312
+ },
4313
+ placeholder: "v1.2.3 / main / abc1234"
4314
+ })
4315
+ ] }),
3823
4316
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3824
4317
  className: panel_module_css_default.dialogActions,
3825
4318
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -3830,9 +4323,9 @@ window.__ModuleLoader__.load({
3830
4323
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3831
4324
  type: "button",
3832
4325
  className: panel_module_css_default.button + " " + panel_module_css_default.primary,
3833
- disabled: busy,
4326
+ disabled: busy || choice.loading || effective === "",
3834
4327
  onClick: onConfirm,
3835
- children: tt("market.branchConfirm")
4328
+ children: busy ? tt("source.syncing") : tt("market.versionConfirm", { ref: effective })
3836
4329
  })]
3837
4330
  })
3838
4331
  ]
@@ -3978,10 +4471,19 @@ window.__ModuleLoader__.load({
3978
4471
  rel: "noreferrer",
3979
4472
  children: record.repo
3980
4473
  }),
3981
- record.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4475
+ type: "button",
3982
4476
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
3983
- children: record.ref
3984
- }) : null,
4477
+ style: {
4478
+ cursor: "pointer",
4479
+ fontFamily: "inherit"
4480
+ },
4481
+ title: tt("market.versionHint"),
4482
+ onClick: () => {
4483
+ hub.openVersionDialog(record.repo);
4484
+ },
4485
+ children: record.ref ?? tt("market.unpinned")
4486
+ }),
3985
4487
  installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3986
4488
  className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
3987
4489
  children: tt("market.installed", { count: installedCount })
@@ -4230,279 +4732,291 @@ window.__ModuleLoader__.load({
4230
4732
  return true;
4231
4733
  }).sort((a, b) => a.name.localeCompare(b.name));
4232
4734
  const paged = filtered.slice(0, visibleCount);
4233
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4234
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
4235
- children: [
4236
- tt("repo.ready", { count: entries.length }),
4237
- " · ",
4238
- repoDiscoverState.data.ref !== null ? `ref ${repoDiscoverState.data.ref}` : ""
4239
- ]
4240
- }), entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4241
- className: panel_module_css_default.empty,
4242
- children: tt("repo.empty")
4243
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4244
- (() => {
4245
- const order = /* @__PURE__ */ new Map([["skills", 0], ["design-templates", 1]]);
4246
- const roots = [...new Set(entries.map((e) => e.root))].sort((a, b) => {
4247
- const ao = order.get(a) ?? 99;
4248
- const bo = order.get(b) ?? 99;
4249
- return ao !== bo ? ao - bo : a.localeCompare(b);
4250
- });
4251
- const counts = /* @__PURE__ */ new Map();
4252
- for (const e of entries) counts.set(e.root, (counts.get(e.root) ?? 0) + 1);
4253
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4254
- style: {
4255
- display: "flex",
4256
- gap: 8,
4257
- marginBottom: 8,
4258
- flexWrap: "wrap"
4259
- },
4260
- children: [
4261
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4262
- className: panel_module_css_default.input,
4263
- style: {
4264
- flex: 1,
4265
- minWidth: 140
4266
- },
4267
- value: repoSearch,
4268
- onChange: (e) => setRepoSearch(e.target.value),
4269
- placeholder: "搜索技能名…"
4270
- }),
4271
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4272
- type: "button",
4273
- className: panel_module_css_default.button + (repoFilter === "all" ? " " + panel_module_css_default.primary : ""),
4274
- style: {
4275
- padding: "6px 10px",
4276
- fontSize: 12
4277
- },
4278
- onClick: () => setRepoFilter("all"),
4279
- children: ["全部 ", entries.length]
4280
- }),
4281
- roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4282
- type: "button",
4283
- className: panel_module_css_default.button + (repoFilter === root ? " " + panel_module_css_default.primary : ""),
4284
- style: {
4285
- padding: "6px 10px",
4286
- fontSize: 12
4287
- },
4288
- onClick: () => setRepoFilter(root),
4289
- children: [
4290
- root,
4291
- " ",
4292
- counts.get(root) ?? 0
4293
- ]
4294
- }, root))
4295
- ]
4296
- });
4297
- })(),
4298
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4299
- className: panel_module_css_default.hintLine,
4735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4737
+ className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
4738
+ children: [
4739
+ tt("repo.ready", { count: entries.length }),
4740
+ " · ",
4741
+ repoDiscoverState.data.ref !== null ? `ref ${repoDiscoverState.data.ref}` : ""
4742
+ ]
4743
+ }),
4744
+ repoDiscoverState.data.truncated === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4745
+ className: panel_module_css_default.errorBanner,
4300
4746
  style: {
4301
- display: "flex",
4302
- justifyContent: "space-between",
4303
- flexWrap: "wrap",
4304
- gap: 8
4747
+ background: "rgba(210,153,34,0.08)",
4748
+ color: "#d29922",
4749
+ borderLeft: "3px solid #d29922"
4305
4750
  },
4306
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
4307
- "已选 ",
4308
- selected.length,
4309
- "/",
4310
- entries.length,
4311
- " · ",
4312
- formatBytes(selectedBytes),
4313
- " · 显示 ",
4314
- paged.length,
4315
- "/",
4316
- filtered.length,
4317
- "(过滤后)"
4318
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4751
+ children: tt("repo.truncated")
4752
+ }) : null,
4753
+ entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4754
+ className: panel_module_css_default.empty,
4755
+ children: tt("repo.empty")
4756
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4757
+ (() => {
4758
+ const order = /* @__PURE__ */ new Map([["skills", 0], ["design-templates", 1]]);
4759
+ const roots = [...new Set(entries.map((e) => e.root))].sort((a, b) => {
4760
+ const ao = order.get(a) ?? 99;
4761
+ const bo = order.get(b) ?? 99;
4762
+ return ao !== bo ? ao - bo : a.localeCompare(b);
4763
+ });
4764
+ const counts = /* @__PURE__ */ new Map();
4765
+ for (const e of entries) counts.set(e.root, (counts.get(e.root) ?? 0) + 1);
4766
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4767
+ style: {
4768
+ display: "flex",
4769
+ gap: 8,
4770
+ marginBottom: 8,
4771
+ flexWrap: "wrap"
4772
+ },
4773
+ children: [
4774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4775
+ className: panel_module_css_default.input,
4776
+ style: {
4777
+ flex: 1,
4778
+ minWidth: 140
4779
+ },
4780
+ value: repoSearch,
4781
+ onChange: (e) => setRepoSearch(e.target.value),
4782
+ placeholder: "搜索技能名…"
4783
+ }),
4784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4785
+ type: "button",
4786
+ className: panel_module_css_default.button + (repoFilter === "all" ? " " + panel_module_css_default.primary : ""),
4787
+ style: {
4788
+ padding: "6px 10px",
4789
+ fontSize: 12
4790
+ },
4791
+ onClick: () => setRepoFilter("all"),
4792
+ children: ["全部 ", entries.length]
4793
+ }),
4794
+ roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4795
+ type: "button",
4796
+ className: panel_module_css_default.button + (repoFilter === root ? " " + panel_module_css_default.primary : ""),
4797
+ style: {
4798
+ padding: "6px 10px",
4799
+ fontSize: 12
4800
+ },
4801
+ onClick: () => setRepoFilter(root),
4802
+ children: [
4803
+ root,
4804
+ " ",
4805
+ counts.get(root) ?? 0
4806
+ ]
4807
+ }, root))
4808
+ ]
4809
+ });
4810
+ })(),
4811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4812
+ className: panel_module_css_default.hintLine,
4319
4813
  style: {
4320
4814
  display: "flex",
4321
- gap: 6
4815
+ justifyContent: "space-between",
4816
+ flexWrap: "wrap",
4817
+ gap: 8
4322
4818
  },
4323
- children: [
4324
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4325
- type: "button",
4326
- className: panel_module_css_default.button,
4327
- style: {
4328
- padding: "4px 8px",
4329
- fontSize: 11
4330
- },
4331
- onClick: () => {
4332
- setRepoSelected(new Set(filtered.filter((e) => !isExisting(e)).slice(0, visibleCount).map((e) => e.path)));
4333
- },
4334
- children: "全选当前显示"
4335
- }),
4336
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4337
- type: "button",
4338
- className: panel_module_css_default.button,
4339
- style: {
4340
- padding: "4px 8px",
4341
- fontSize: 11
4342
- },
4343
- onClick: () => {
4344
- setRepoSelected(new Set(entries.filter((e) => !isExisting(e)).map((e) => e.path)));
4345
- },
4346
- children: ["全选全部 ", entries.filter((e) => !isExisting(e)).length]
4347
- }),
4348
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4349
- type: "button",
4350
- className: panel_module_css_default.button,
4351
- style: {
4352
- padding: "4px 8px",
4353
- fontSize: 11
4354
- },
4355
- onClick: () => {
4356
- setRepoSelected(/* @__PURE__ */ new Set());
4357
- },
4358
- children: tt("repo.clearAll")
4359
- })
4360
- ]
4361
- })]
4362
- }),
4363
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4364
- className: panel_module_css_default.scanList,
4365
- children: paged.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4366
- className: panel_module_css_default.empty,
4367
- style: { padding: 20 },
4368
- children: "无匹配技能"
4369
- }) : paged.map((entry) => {
4370
- const existing = isExisting(entry);
4371
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4372
- className: panel_module_css_default.row + (existing ? " " + panel_module_css_default.rowMuted : ""),
4819
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
4820
+ "已选 ",
4821
+ selected.length,
4822
+ "/",
4823
+ entries.length,
4824
+ " · ",
4825
+ formatBytes(selectedBytes),
4826
+ " · 显示 ",
4827
+ paged.length,
4828
+ "/",
4829
+ filtered.length,
4830
+ "(过滤后)"
4831
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4832
+ style: {
4833
+ display: "flex",
4834
+ gap: 6
4835
+ },
4373
4836
  children: [
4374
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4375
- type: "checkbox",
4376
- checked: repoSelected.has(entry.path),
4377
- disabled: existing,
4378
- onChange: (event) => {
4379
- toggleRepoSelected(entry.path, event.target.checked);
4380
- }
4837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4838
+ type: "button",
4839
+ className: panel_module_css_default.button,
4840
+ style: {
4841
+ padding: "4px 8px",
4842
+ fontSize: 11
4843
+ },
4844
+ onClick: () => {
4845
+ setRepoSelected(new Set(filtered.filter((e) => !isExisting(e)).slice(0, visibleCount).map((e) => e.path)));
4846
+ },
4847
+ children: "全选当前显示"
4381
4848
  }),
4382
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4383
- className: panel_module_css_default.rowMain,
4384
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4385
- className: panel_module_css_default.rowName,
4386
- children: entry.name
4387
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4388
- className: panel_module_css_default.rowDesc,
4389
- children: [
4390
- entry.dir,
4391
- " · ",
4392
- tt("repo.files", {
4393
- count: entry.fileCount,
4394
- size: formatBytes(entry.totalBytes)
4395
- })
4396
- ]
4397
- })]
4849
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4850
+ type: "button",
4851
+ className: panel_module_css_default.button,
4852
+ style: {
4853
+ padding: "4px 8px",
4854
+ fontSize: 11
4855
+ },
4856
+ onClick: () => {
4857
+ setRepoSelected(new Set(entries.filter((e) => !isExisting(e)).map((e) => e.path)));
4858
+ },
4859
+ children: ["全选全部 ", entries.filter((e) => !isExisting(e)).length]
4398
4860
  }),
4399
- existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4400
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
4401
- children: tt("repo.existing")
4402
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4403
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4404
- children: entry.origin
4861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4862
+ type: "button",
4863
+ className: panel_module_css_default.button,
4864
+ style: {
4865
+ padding: "4px 8px",
4866
+ fontSize: 11
4867
+ },
4868
+ onClick: () => {
4869
+ setRepoSelected(/* @__PURE__ */ new Set());
4870
+ },
4871
+ children: tt("repo.clearAll")
4405
4872
  })
4406
4873
  ]
4407
- }, entry.path);
4408
- })
4409
- }),
4410
- visibleCount < filtered.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4411
- style: {
4412
- textAlign: "center",
4413
- marginTop: 8
4414
- },
4415
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4416
- type: "button",
4417
- className: panel_module_css_default.button,
4418
- onClick: () => setVisibleCount((n) => n + 50),
4419
- children: [
4420
- "加载更多 50 (剩余 ",
4421
- filtered.length - visibleCount,
4422
- ")"
4423
- ]
4424
- })
4425
- }) : null,
4426
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4427
- className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
4428
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4429
- type: "button",
4430
- className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4431
- disabled: selected.length === 0 || repoImporting,
4432
- onClick: () => {
4433
- importRepo();
4434
- },
4435
- children: repoImporting ? tt("repo.importing") : `${tt("repo.import", { count: selected.length })} · ${formatBytes(selectedBytes)}`
4436
- }), repoImporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4437
- type: "button",
4438
- className: panel_module_css_default.button,
4439
- onClick: () => {
4440
- cancelImport();
4441
- },
4442
- children: tt("repo.cancel")
4443
- }) : null]
4444
- }),
4445
- repoResult !== null && repoResult.status === "running" ? (() => {
4446
- 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;
4447
- const speed = formatSpeed(repoResult.bytesPerSecond);
4448
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4449
- className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
4450
- children: [
4451
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4452
- style: {
4453
- display: "flex",
4454
- alignItems: "center",
4455
- gap: 8,
4456
- marginBottom: 6
4457
- },
4458
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4459
- className: panel_module_css_default.scanProgressTrack,
4460
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4461
- className: panel_module_css_default.scanProgressFill,
4462
- style: { width: `${pct}%` }
4874
+ })]
4875
+ }),
4876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4877
+ className: panel_module_css_default.scanList,
4878
+ children: paged.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4879
+ className: panel_module_css_default.empty,
4880
+ style: { padding: 20 },
4881
+ children: "无匹配技能"
4882
+ }) : paged.map((entry) => {
4883
+ const existing = isExisting(entry);
4884
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4885
+ className: panel_module_css_default.row + (existing ? " " + panel_module_css_default.rowMuted : ""),
4886
+ children: [
4887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4888
+ type: "checkbox",
4889
+ checked: repoSelected.has(entry.path),
4890
+ disabled: existing,
4891
+ onChange: (event) => {
4892
+ toggleRepoSelected(entry.path, event.target.checked);
4893
+ }
4894
+ }),
4895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4896
+ className: panel_module_css_default.rowMain,
4897
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4898
+ className: panel_module_css_default.rowName,
4899
+ children: entry.name
4900
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4901
+ className: panel_module_css_default.rowDesc,
4902
+ children: [
4903
+ entry.dir,
4904
+ " · ",
4905
+ tt("repo.files", {
4906
+ count: entry.fileCount,
4907
+ size: formatBytes(entry.totalBytes)
4908
+ })
4909
+ ]
4910
+ })]
4911
+ }),
4912
+ existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4913
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
4914
+ children: tt("repo.existing")
4915
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4916
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4917
+ children: entry.origin
4463
4918
  })
4464
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4919
+ ]
4920
+ }, entry.path);
4921
+ })
4922
+ }),
4923
+ visibleCount < filtered.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4924
+ style: {
4925
+ textAlign: "center",
4926
+ marginTop: 8
4927
+ },
4928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4929
+ type: "button",
4930
+ className: panel_module_css_default.button,
4931
+ onClick: () => setVisibleCount((n) => n + 50),
4932
+ children: [
4933
+ "加载更多 50 (剩余 ",
4934
+ filtered.length - visibleCount,
4935
+ ")"
4936
+ ]
4937
+ })
4938
+ }) : null,
4939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4940
+ className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
4941
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4942
+ type: "button",
4943
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4944
+ disabled: selected.length === 0 || repoImporting,
4945
+ onClick: () => {
4946
+ importRepo();
4947
+ },
4948
+ children: repoImporting ? tt("repo.importing") : `${tt("repo.import", { count: selected.length })} · ${formatBytes(selectedBytes)}`
4949
+ }), repoImporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4950
+ type: "button",
4951
+ className: panel_module_css_default.button,
4952
+ onClick: () => {
4953
+ cancelImport();
4954
+ },
4955
+ children: tt("repo.cancel")
4956
+ }) : null]
4957
+ }),
4958
+ repoResult !== null && repoResult.status === "running" ? (() => {
4959
+ 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;
4960
+ const speed = formatSpeed(repoResult.bytesPerSecond);
4961
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4962
+ className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
4963
+ children: [
4964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4465
4965
  style: {
4466
- fontSize: 11,
4467
- opacity: .75,
4468
- minWidth: 32,
4469
- textAlign: "right"
4966
+ display: "flex",
4967
+ alignItems: "center",
4968
+ gap: 8,
4969
+ marginBottom: 6
4470
4970
  },
4471
- children: [pct, "%"]
4472
- })]
4473
- }),
4474
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4475
- className: panel_module_css_default.hintLine,
4476
- children: [
4477
- formatBytes(repoResult.downloadedBytes),
4478
- " / ",
4479
- formatBytes(repoResult.totalBytes),
4480
- speed !== "" ? ` · ${speed}` : "",
4481
- " · ",
4482
- repoResult.done,
4483
- "/",
4484
- repoResult.total,
4485
- " 个技能"
4486
- ]
4487
- }),
4488
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4489
- className: panel_module_css_default.hintLine,
4490
- style: { marginTop: 4 },
4491
- children: [repoResult.current !== void 0 ? tt("repo.importingCurrent", {
4492
- name: repoResult.current,
4493
- done: repoResult.done + 1,
4494
- total: repoResult.total
4495
- }) : tt("repo.importing"), repoResult.currentFile !== void 0 ? ` · ${repoResult.currentFile}` : ""]
4496
- }),
4497
- repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4498
- className: panel_module_css_default.errorBanner,
4499
- style: { marginTop: 6 },
4500
- children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
4501
- }) : null
4502
- ]
4503
- });
4504
- })() : null
4505
- ] })] });
4971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4972
+ className: panel_module_css_default.scanProgressTrack,
4973
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4974
+ className: panel_module_css_default.scanProgressFill,
4975
+ style: { width: `${pct}%` }
4976
+ })
4977
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4978
+ style: {
4979
+ fontSize: 11,
4980
+ opacity: .75,
4981
+ minWidth: 32,
4982
+ textAlign: "right"
4983
+ },
4984
+ children: [pct, "%"]
4985
+ })]
4986
+ }),
4987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4988
+ className: panel_module_css_default.hintLine,
4989
+ children: [
4990
+ formatBytes(repoResult.downloadedBytes),
4991
+ " / ",
4992
+ formatBytes(repoResult.totalBytes),
4993
+ speed !== "" ? ` · ${speed}` : "",
4994
+ " · ",
4995
+ repoResult.done,
4996
+ "/",
4997
+ repoResult.total,
4998
+ " 个技能"
4999
+ ]
5000
+ }),
5001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5002
+ className: panel_module_css_default.hintLine,
5003
+ style: { marginTop: 4 },
5004
+ children: [repoResult.current !== void 0 ? tt("repo.importingCurrent", {
5005
+ name: repoResult.current,
5006
+ done: repoResult.done + 1,
5007
+ total: repoResult.total
5008
+ }) : tt("repo.importing"), repoResult.currentFile !== void 0 ? ` · ${repoResult.currentFile}` : ""]
5009
+ }),
5010
+ repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5011
+ className: panel_module_css_default.errorBanner,
5012
+ style: { marginTop: 6 },
5013
+ children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
5014
+ }) : null
5015
+ ]
5016
+ });
5017
+ })() : null
5018
+ ] })
5019
+ ] });
4506
5020
  })() : null,
4507
5021
  repoResult !== null && repoResult.status !== "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4508
5022
  className: repoResult.status === "cancelled" ? panel_module_css_default.errorBanner + " " + panel_module_css_default.actionsTop : panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
@@ -4609,6 +5123,7 @@ window.__ModuleLoader__.load({
4609
5123
  const [tab, setTab] = (0, react.useState)("sources");
4610
5124
  const [skillView, setSkillView] = (0, react.useState)("groups");
4611
5125
  const [sourceFilter, setSourceFilter] = (0, react.useState)("all");
5126
+ const [invocationFilter, setInvocationFilter] = (0, react.useState)("all");
4612
5127
  const [sortKey, setSortKey] = (0, react.useState)("name");
4613
5128
  const [marketState, setMarketState] = (0, react.useState)({
4614
5129
  status: "loading",
@@ -4639,8 +5154,9 @@ window.__ModuleLoader__.load({
4639
5154
  const [newTagName, setNewTagName] = (0, react.useState)("");
4640
5155
  const [tagBusy, setTagBusy] = (0, react.useState)(false);
4641
5156
  const [editSearch, setEditSearch] = (0, react.useState)("");
4642
- /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。 */
5157
+ /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。技能总数 >80 时首次加载自动折叠 personal。 */
4643
5158
  const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
5159
+ const autoCollapsedPersonal = (0, react.useRef)(false);
4644
5160
  /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
4645
5161
  const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
4646
5162
  const [showLegend, setShowLegend] = (0, react.useState)(false);
@@ -4665,6 +5181,10 @@ window.__ModuleLoader__.load({
4665
5181
  try {
4666
5182
  const next = await api.catalog(workspace !== "" ? { cwd: workspace } : void 0);
4667
5183
  setCatalog(next);
5184
+ if (!autoCollapsedPersonal.current && next.skills.length + next.disabled.length > 80) {
5185
+ autoCollapsedPersonal.current = true;
5186
+ setCollapsedGroups((previous) => new Set(previous).add("uncategorized-source"));
5187
+ }
4668
5188
  setLoadError(null);
4669
5189
  } catch (error) {
4670
5190
  setLoadError(errorMessage(error));
@@ -4991,6 +5511,25 @@ window.__ModuleLoader__.load({
4991
5511
  load,
4992
5512
  loadSources
4993
5513
  ]);
5514
+ const [fixingPaths, setFixingPaths] = (0, react.useState)(/* @__PURE__ */ new Set());
5515
+ const fixDiagnostic = (0, react.useCallback)(async (path) => {
5516
+ setFixingPaths((previous) => new Set(previous).add(path));
5517
+ setLoadError(null);
5518
+ try {
5519
+ if (!window.confirm(tt("diag.fixConfirm", { path }))) return;
5520
+ await api.fixDiagnostic(path);
5521
+ await load();
5522
+ setSuccessBanner(tt("diag.fixed"));
5523
+ } catch (error) {
5524
+ setLoadError(errorMessage(error));
5525
+ } finally {
5526
+ setFixingPaths((previous) => {
5527
+ const next = new Set(previous);
5528
+ next.delete(path);
5529
+ return next;
5530
+ });
5531
+ }
5532
+ }, [api, load]);
4994
5533
  /** 打开单个技能的删除确认(移入回收站,可恢复)。 */
4995
5534
  const requestDeleteSkill = (0, react.useCallback)((name) => {
4996
5535
  setDeleteSkillDialog(name);
@@ -5274,6 +5813,7 @@ window.__ModuleLoader__.load({
5274
5813
  status: "ready",
5275
5814
  data
5276
5815
  });
5816
+ loadMarket();
5277
5817
  } catch (error) {
5278
5818
  setRepoDiscoverState({
5279
5819
  status: "error",
@@ -5282,7 +5822,7 @@ window.__ModuleLoader__.load({
5282
5822
  } finally {
5283
5823
  setScanningRepo(null);
5284
5824
  }
5285
- }, [api]);
5825
+ }, [api, loadMarket]);
5286
5826
  /** 确认分支选择:持久化 ref 后重新扫描。 */
5287
5827
  const confirmBranchChoice = (0, react.useCallback)(async () => {
5288
5828
  if (branchChoice === null) return;
@@ -5302,6 +5842,59 @@ window.__ModuleLoader__.load({
5302
5842
  branchChoice,
5303
5843
  scanRepo
5304
5844
  ]);
5845
+ /** 版本对话框:打开时拉 releases + branches,确认后定版并重扫。 */
5846
+ const [versionDialog, setVersionDialog] = (0, react.useState)(null);
5847
+ const [versionBusy, setVersionBusy] = (0, react.useState)(false);
5848
+ const openVersionDialog = (0, react.useCallback)(async (repo) => {
5849
+ const current = marketState.repos.find((item) => item.repo === repo)?.ref;
5850
+ setVersionDialog({
5851
+ repo,
5852
+ ...current !== void 0 ? { current } : {},
5853
+ releases: [],
5854
+ branches: [],
5855
+ selected: current ?? "",
5856
+ custom: "",
5857
+ loading: true
5858
+ });
5859
+ setLoadError(null);
5860
+ try {
5861
+ const data = await api.marketSourceVersions(repo);
5862
+ setVersionDialog({
5863
+ repo,
5864
+ ...data.current !== void 0 ? { current: data.current } : {},
5865
+ releases: data.releases,
5866
+ branches: data.branches,
5867
+ selected: data.current ?? data.releases[0] ?? data.branches[0] ?? "",
5868
+ custom: "",
5869
+ loading: false
5870
+ });
5871
+ } catch (error) {
5872
+ setLoadError(errorMessage(error));
5873
+ setVersionDialog(null);
5874
+ }
5875
+ }, [api, marketState]);
5876
+ const confirmVersionDialog = (0, react.useCallback)(async () => {
5877
+ if (versionDialog === null) return;
5878
+ const ref = versionDialog.custom.trim() !== "" ? versionDialog.custom.trim() : versionDialog.selected;
5879
+ if (ref === "") return;
5880
+ setVersionBusy(true);
5881
+ setLoadError(null);
5882
+ try {
5883
+ await api.setMarketSourceRef(versionDialog.repo, ref);
5884
+ setVersionDialog(null);
5885
+ await loadMarket();
5886
+ await scanRepo(versionDialog.repo);
5887
+ } catch (error) {
5888
+ setLoadError(errorMessage(error));
5889
+ } finally {
5890
+ setVersionBusy(false);
5891
+ }
5892
+ }, [
5893
+ api,
5894
+ versionDialog,
5895
+ loadMarket,
5896
+ scanRepo
5897
+ ]);
5305
5898
  /** Toggle one repo preview row. */
5306
5899
  const toggleRepoSelected = (0, react.useCallback)((path, checked) => {
5307
5900
  setRepoSelected((previous) => {
@@ -5584,7 +6177,18 @@ window.__ModuleLoader__.load({
5584
6177
  const hasPrivate = skills.some((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
5585
6178
  return [...repos, ...hasPrivate ? [PRIVATE_SOURCE] : []];
5586
6179
  }, [catalog, origins]);
5587
- const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => normalized.length === 0 || skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized)), [catalog, normalized]);
6180
+ const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => {
6181
+ if (invocationFilter === "model" && !skill.invocation.modelInvocable) return false;
6182
+ if (invocationFilter === "user" && !skill.invocation.userInvocable) return false;
6183
+ if (normalized.length === 0) return true;
6184
+ return skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized) || skill.displayName?.toLocaleLowerCase().includes(normalized) || skill.shortDescription?.toLocaleLowerCase().includes(normalized);
6185
+ }), [
6186
+ catalog,
6187
+ normalized,
6188
+ invocationFilter
6189
+ ]);
6190
+ /** Rows actually rendered with a shortened description (shortDescription in use). */
6191
+ const shortenedCount = (0, react.useMemo)(() => filtered.filter((skill) => skill.shortDescription !== void 0).length, [filtered]);
5588
6192
  return {
5589
6193
  catalog,
5590
6194
  loading,
@@ -5614,6 +6218,7 @@ window.__ModuleLoader__.load({
5614
6218
  tab,
5615
6219
  skillView,
5616
6220
  sourceFilter,
6221
+ invocationFilter,
5617
6222
  sortKey,
5618
6223
  marketState,
5619
6224
  marketCheck,
@@ -5644,6 +6249,8 @@ window.__ModuleLoader__.load({
5644
6249
  subdividedProjects,
5645
6250
  showLegend,
5646
6251
  editMode,
6252
+ versionDialog,
6253
+ versionBusy,
5647
6254
  actionNames,
5648
6255
  viewNames,
5649
6256
  normalized,
@@ -5655,6 +6262,7 @@ window.__ModuleLoader__.load({
5655
6262
  sortKey,
5656
6263
  uses
5657
6264
  ]),
6265
+ shortenedCount,
5658
6266
  setLoadError,
5659
6267
  setSuccessBanner,
5660
6268
  setSearch,
@@ -5669,6 +6277,7 @@ window.__ModuleLoader__.load({
5669
6277
  setTab,
5670
6278
  setSkillView,
5671
6279
  setSourceFilter,
6280
+ setInvocationFilter,
5672
6281
  setSortKey,
5673
6282
  setBranchChoice,
5674
6283
  setMarketSyncDialog,
@@ -5686,6 +6295,7 @@ window.__ModuleLoader__.load({
5686
6295
  setEditSearch,
5687
6296
  setShowLegend,
5688
6297
  setEditMode,
6298
+ setVersionDialog,
5689
6299
  toggleGroupCollapse,
5690
6300
  toggleSubdivide,
5691
6301
  checkUpdate,
@@ -5702,6 +6312,15 @@ window.__ModuleLoader__.load({
5702
6312
  requestDelete,
5703
6313
  restoreTrash,
5704
6314
  clearTrash,
6315
+ fixingPaths,
6316
+ fixDiagnostic,
6317
+ clearListFilters: (0, react.useCallback)(() => {
6318
+ setSearch("");
6319
+ setSourceFilter("all");
6320
+ setInvocationFilter("all");
6321
+ }, []),
6322
+ openVersionDialog,
6323
+ confirmVersionDialog,
5705
6324
  requestDeleteSkill,
5706
6325
  runDeleteSkill,
5707
6326
  requestDeleteGroup,
@@ -5748,25 +6367,35 @@ window.__ModuleLoader__.load({
5748
6367
  /** 工作区输入草稿:回车才应用,避免每次按键都触发目录重拉。 */
5749
6368
  const [workspaceDraft, setWorkspaceDraft] = (0, react.useState)("");
5750
6369
  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;
5751
- if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
5752
- detail,
5753
- hubConfig,
5754
- uses,
5755
- groupsState,
5756
- sourcesState,
5757
- sourceCheck,
5758
- checkingSource,
5759
- syncingSource,
5760
- loading: detailLoading,
5761
- onBack: () => {
5762
- setDetail(null);
5763
- },
5764
- onCheck: (repo) => {
5765
- checkSources(repo);
5766
- },
5767
- onSync: requestSync,
5768
- onFollowDelete: requestDelete
5769
- });
6370
+ const { shortenedCount, fixingPaths, clearListFilters } = hub;
6371
+ if (detail !== null) {
6372
+ const disabledRecord = catalog?.disabled.find((record) => record.name === detail.name);
6373
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
6374
+ detail,
6375
+ hubConfig,
6376
+ uses,
6377
+ groupsState,
6378
+ sourcesState,
6379
+ sourceCheck,
6380
+ checkingSource,
6381
+ syncingSource,
6382
+ loading: detailLoading,
6383
+ disabled: disabledRecord !== void 0,
6384
+ onEnable: disabledRecord !== void 0 ? () => {
6385
+ enableDisabled(disabledRecord).then(() => {
6386
+ setDetail(null);
6387
+ });
6388
+ } : void 0,
6389
+ onBack: () => {
6390
+ setDetail(null);
6391
+ },
6392
+ onCheck: (repo) => {
6393
+ checkSources(repo);
6394
+ },
6395
+ onSync: requestSync,
6396
+ onFollowDelete: requestDelete
6397
+ });
6398
+ }
5770
6399
  if (editingTag !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TagEditorView, {
5771
6400
  tag: editingTag,
5772
6401
  editName,
@@ -5841,6 +6470,20 @@ window.__ModuleLoader__.load({
5841
6470
  className: panel_module_css_default.hint,
5842
6471
  children: tt("panel.incomplete")
5843
6472
  }) : null,
6473
+ catalog !== null && (catalog.duplicateNames?.length ?? 0) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6474
+ type: "button",
6475
+ className: panel_module_css_default.opBtn,
6476
+ title: tt("row.duplicateHint"),
6477
+ onClick: () => {
6478
+ clearListFilters();
6479
+ },
6480
+ children: [
6481
+ "⚠ ",
6482
+ tt("row.duplicate"),
6483
+ "×",
6484
+ (catalog.duplicateNames ?? []).length
6485
+ ]
6486
+ }) : null,
5844
6487
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5845
6488
  className: panel_module_css_default.actions,
5846
6489
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -6131,6 +6774,27 @@ window.__ModuleLoader__.load({
6131
6774
  })
6132
6775
  ]
6133
6776
  }),
6777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6778
+ className: panel_module_css_default.select,
6779
+ value: hub.invocationFilter,
6780
+ onChange: (event) => {
6781
+ hub.setInvocationFilter(event.target.value);
6782
+ },
6783
+ children: [
6784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6785
+ value: "all",
6786
+ children: tt("filter.invocationAll")
6787
+ }),
6788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6789
+ value: "model",
6790
+ children: tt("filter.modelOnly")
6791
+ }),
6792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6793
+ value: "user",
6794
+ children: tt("filter.userOnly")
6795
+ })
6796
+ ]
6797
+ }),
6134
6798
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6135
6799
  className: panel_module_css_default.search,
6136
6800
  value: search,
@@ -6148,6 +6812,23 @@ window.__ModuleLoader__.load({
6148
6812
  })
6149
6813
  ]
6150
6814
  }),
6815
+ catalog !== null && (filtered.length !== catalog.skills.length || hub.invocationFilter !== "all" || sourceFilter !== "all") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6816
+ className: panel_module_css_default.hintLine,
6817
+ style: {
6818
+ margin: "2px 2px 0",
6819
+ display: "flex",
6820
+ gap: 8,
6821
+ flexWrap: "wrap"
6822
+ },
6823
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("filter.showing", {
6824
+ shown: filtered.length,
6825
+ total: catalog.skills.length,
6826
+ filtered: hub.invocationFilter !== "all" || sourceFilter !== "all" ? tt("filter.filteredSuffix") : ""
6827
+ }) }), shortenedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6828
+ title: tt("filter.shortened", { count: shortenedCount }),
6829
+ children: tt("filter.shortened", { count: shortenedCount })
6830
+ }) : null]
6831
+ }) : null,
6151
6832
  filtered.length === 0 && search.trim() !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6152
6833
  className: panel_module_css_default.empty,
6153
6834
  children: tt("panel.empty")
@@ -6202,13 +6883,36 @@ window.__ModuleLoader__.load({
6202
6883
  children: tt("panel.diagnostics")
6203
6884
  }), catalog.diagnostics.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6204
6885
  className: panel_module_css_default.diagRow,
6205
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6206
- className: panel_module_css_default.diagPath,
6207
- children: entry.path
6208
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6209
- className: panel_module_css_default.diagReason,
6210
- children: entry.reason
6211
- })]
6886
+ style: {
6887
+ display: "flex",
6888
+ alignItems: "flex-start",
6889
+ gap: 10
6890
+ },
6891
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6892
+ style: {
6893
+ flex: 1,
6894
+ minWidth: 0
6895
+ },
6896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6897
+ className: panel_module_css_default.diagPath,
6898
+ children: entry.path
6899
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6900
+ className: panel_module_css_default.diagReason,
6901
+ children: entry.reason
6902
+ })]
6903
+ }), entry.fixable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6904
+ type: "button",
6905
+ className: panel_module_css_default.opBtn,
6906
+ disabled: fixingPaths.has(entry.path),
6907
+ onClick: () => {
6908
+ hub.fixDiagnostic(entry.path);
6909
+ },
6910
+ style: {
6911
+ flex: "none",
6912
+ alignSelf: "center"
6913
+ },
6914
+ children: fixingPaths.has(entry.path) ? tt("diag.fixing") : tt("diag.fix")
6915
+ }) : null]
6212
6916
  }, entry.path))]
6213
6917
  }) : null
6214
6918
  ] }) : null,
@@ -6255,6 +6959,28 @@ window.__ModuleLoader__.load({
6255
6959
  confirmBranchChoice();
6256
6960
  }
6257
6961
  }) : null,
6962
+ hub.versionDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VersionChoiceDialog, {
6963
+ choice: hub.versionDialog,
6964
+ busy: hub.versionBusy,
6965
+ onSelect: (selected) => {
6966
+ hub.setVersionDialog({
6967
+ ...hub.versionDialog,
6968
+ selected
6969
+ });
6970
+ },
6971
+ onCustom: (custom) => {
6972
+ hub.setVersionDialog({
6973
+ ...hub.versionDialog,
6974
+ custom
6975
+ });
6976
+ },
6977
+ onCancel: () => {
6978
+ hub.setVersionDialog(null);
6979
+ },
6980
+ onConfirm: () => {
6981
+ hub.confirmVersionDialog();
6982
+ }
6983
+ }) : null,
6258
6984
  marketSyncDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarketSyncDialog, {
6259
6985
  dialog: marketSyncDialog,
6260
6986
  busy: syncBusy,
@@ -6333,7 +7059,8 @@ window.__ModuleLoader__.load({
6333
7059
  "locale",
6334
7060
  "connection",
6335
7061
  "remote",
6336
- "settingsScope"
7062
+ "settingsScope",
7063
+ "inputTriggers"
6337
7064
  ];
6338
7065
  /**
6339
7066
  * Mount the settings card and the skill hub section.
@@ -6346,7 +7073,9 @@ window.__ModuleLoader__.load({
6346
7073
  }), "dsh-skill-hub: dictionaries");
6347
7074
  const t = ctx.locale.bind(NS);
6348
7075
  const api = new SkillHubApi();
6349
- const settingsCard = new SkillHubSettingsCardController(ctx.settingsScope.bind({ namespace: NS }));
7076
+ const scope = ctx.settingsScope.bind({ namespace: NS });
7077
+ const settingsCard = new SkillHubSettingsCardController(scope);
7078
+ ctx.effect(() => setupSkillSlashDots(ctx, api, scope), "dsh-skill-hub: slash dots");
6350
7079
  ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
6351
7080
  name: "settings.plugin.item",
6352
7081
  key: NS,