dsh-skill-hub 0.2.0 → 0.2.2

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 (45) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +181 -249
  3. package/README.zh.md +275 -0
  4. package/lib/client.js +395 -257
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +126 -26
  7. package/lib/types/client/SkillHubSettingsCard.d.ts +23 -17
  8. package/lib/types/client/api.d.ts +9 -3
  9. package/lib/types/client/grouping.d.ts +3 -0
  10. package/lib/types/client/index.d.ts +12 -6
  11. package/lib/types/client/locales.d.ts +8 -4
  12. package/lib/types/client/market-catalog.d.ts +1 -1
  13. package/lib/types/client/panel/MarketView.d.ts +5 -5
  14. package/lib/types/client/panel/SkillHubPanel.d.ts +1 -1
  15. package/lib/types/client/panel/SourcesView.d.ts +5 -3
  16. package/lib/types/client/panel/useSkillHub.d.ts +4 -0
  17. package/lib/types/client/settings-card.d.ts +5 -4
  18. package/lib/types/index.d.ts +12 -1
  19. package/lib/types/protocol.d.ts +23 -0
  20. package/lib/types/skillfs.d.ts +1 -1
  21. package/lib/types/update.d.ts +1 -1
  22. package/package.json +28 -27
  23. package/src/client/SkillHubSettingsCard.tsx +25 -14
  24. package/src/client/api.ts +9 -6
  25. package/src/client/grouping.test.ts +12 -0
  26. package/src/client/grouping.ts +10 -1
  27. package/src/client/index.tsx +25 -19
  28. package/src/client/locales.ts +16 -8
  29. package/src/client/market-catalog.ts +1 -1
  30. package/src/client/panel/MarketView.tsx +77 -64
  31. package/src/client/panel/SkillHubPanel.tsx +18 -1
  32. package/src/client/panel/SkillRow.tsx +17 -2
  33. package/src/client/panel/SourcesView.tsx +92 -5
  34. package/src/client/panel/panel.module.css +9 -1
  35. package/src/client/panel/useSkillHub.ts +25 -11
  36. package/src/client/settings-card.tsx +5 -4
  37. package/src/index.ts +78 -31
  38. package/src/protocol.ts +24 -0
  39. package/src/routes.test.ts +55 -0
  40. package/src/routes.ts +90 -10
  41. package/src/skillfs.ts +1 -1
  42. package/src/update.ts +1 -1
  43. package/lib/types/client/api-config-scope.d.ts +0 -48
  44. package/src/client/api-config-scope.test.ts +0 -79
  45. package/src/client/api-config-scope.ts +0 -119
package/lib/client.js CHANGED
@@ -88,11 +88,14 @@ window.__ModuleLoader__.load({
88
88
  }
89
89
  /** The browser half's only data entry point. */
90
90
  var SkillHubApi = class {
91
- async catalog() {
92
- return readJson(await fetchWithTimeout(SKILL_HUB_API.catalog));
91
+ /** Catalog lookup options (cwd selects a workspace's project skills). */
92
+ catalog(options) {
93
+ return fetchWithTimeout(options?.cwd !== void 0 && options.cwd !== "" ? SKILL_HUB_API.catalog + "?cwd=" + encodeURIComponent(options.cwd) : SKILL_HUB_API.catalog).then((response) => readJson(response));
93
94
  }
94
- async skill(name) {
95
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.skill + "?name=" + encodeURIComponent(name)))).skill;
95
+ /** One skill's detail (cwd selects a workspace's project skills). */
96
+ async skill(name, options) {
97
+ const cwd = options?.cwd !== void 0 && options.cwd !== "" ? "&cwd=" + encodeURIComponent(options.cwd) : "";
98
+ return (await readJson(await fetchWithTimeout(SKILL_HUB_API.skill + "?name=" + encodeURIComponent(name) + cwd))).skill;
96
99
  }
97
100
  /** Move one writable skill into the restorable trash. */
98
101
  async deleteSkill(name) {
@@ -135,7 +138,7 @@ window.__ModuleLoader__.load({
135
138
  body: JSON.stringify(payload)
136
139
  }));
137
140
  }
138
- /** The user's added market sources (codex-style repo slugs). */
141
+ /** The user's added market sources. */
139
142
  async market() {
140
143
  return readJson(await fetchWithTimeout(SKILL_HUB_API.market));
141
144
  }
@@ -294,102 +297,6 @@ window.__ModuleLoader__.load({
294
297
  }
295
298
  };
296
299
  //#endregion
297
- //#region src/client/api-config-scope.ts
298
- /** Model-invocable dot color default. Single source for the TS side; the
299
- * panel's CSS mirrors it via --hub-model (panel.module.css). */
300
- const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
301
- /** User-invocable dot color default. Single source for the TS side; the
302
- * panel's CSS mirrors it via --hub-user (panel.module.css). */
303
- const DEFAULT_DOT_USER_COLOR = "#3fb950";
304
- /** Built-in defaults every field inherits until the user overrides it. */
305
- const DEFAULTS = {
306
- enabled: true,
307
- announceToAgent: true,
308
- showUseCount: true,
309
- showUseTime: true,
310
- showGroupSummary: true
311
- };
312
- /** Boolean config fields (color fields are strings). */
313
- const BOOLEAN_FIELDS = /* @__PURE__ */ new Set([
314
- "enabled",
315
- "announceToAgent",
316
- "showUseCount",
317
- "showUseTime",
318
- "showGroupSummary"
319
- ]);
320
- /** Config fields the card edits (used to guard set/unset field names). */
321
- const FIELDS = /* @__PURE__ */ new Set([
322
- "enabled",
323
- "announceToAgent",
324
- "dotModelColor",
325
- "dotUserColor",
326
- "showUseCount",
327
- "showUseTime",
328
- "showGroupSummary"
329
- ]);
330
- /** FormScope-compatible reactive config handle (see module doc). */
331
- var ApiConfigScope = class {
332
- api;
333
- status = "loading";
334
- value = { ...DEFAULTS };
335
- saved = {};
336
- listeners = /* @__PURE__ */ new Set();
337
- /** @param api - the browser-half API client (shared with the skill panel). */
338
- constructor(api) {
339
- this.api = api;
340
- this.load();
341
- }
342
- async load() {
343
- try {
344
- const response = await this.api.config();
345
- this.value = response.config;
346
- this.saved = { ...response.saved };
347
- this.status = "ready";
348
- } catch {
349
- this.status = "unavailable";
350
- }
351
- this.publish();
352
- }
353
- subscribe(listener) {
354
- this.listeners.add(listener);
355
- return () => {
356
- this.listeners.delete(listener);
357
- };
358
- }
359
- getSnapshot() {
360
- return {
361
- status: this.status,
362
- writable: true,
363
- value: { ...this.value },
364
- base: { ...DEFAULTS },
365
- user: { ...this.saved }
366
- };
367
- }
368
- /** Write one field through the config route and adopt the persisted state. */
369
- async set(field, value) {
370
- if (!FIELDS.has(field)) return;
371
- const patch = BOOLEAN_FIELDS.has(field) ? { [field]: Boolean(value) } : { [field]: typeof value === "string" ? value : void 0 };
372
- await this.apply(patch);
373
- }
374
- /** Clear one field's override so it re-inherits the default. */
375
- async unset(field) {
376
- if (!FIELDS.has(field)) return;
377
- await this.apply({ [field]: void 0 });
378
- }
379
- /** Persist a patch, then re-seed from the route's fresh response. */
380
- async apply(patch) {
381
- const request = {};
382
- for (const [key, value] of Object.entries(patch)) request[key] = value === void 0 ? null : value;
383
- const response = await this.api.saveConfig(request);
384
- this.value = response.config;
385
- this.saved = { ...response.saved };
386
- this.publish();
387
- }
388
- publish() {
389
- for (const listener of this.listeners) listener();
390
- }
391
- };
392
- //#endregion
393
300
  //#region src/client/locales.ts
394
301
  /**
395
302
  * dsh-skill-hub surface copy (zh/en). The dictionary type is re-exported
@@ -419,19 +326,19 @@ window.__ModuleLoader__.load({
419
326
  "panel.emptyAll": "本地还没有技能。点击「新建技能」创建第一个。",
420
327
  "panel.loading": "读取中…",
421
328
  "panel.incomplete": "(目录可能不完整)",
422
- "market.addHint": "像 codex 一样添加仓库源:输入 owner/repo 或 GitHub 链接,扫描其中的技能并一键导入;导入后自动跟踪上游更新。",
329
+ "panel.workspacePlaceholder": "工作区路径(回车应用)",
330
+ "panel.workspaceHint": "填写项目路径后,面板同时显示该项目 .dsh/skills 与 .agents/skills 下的技能(只读);留空回车恢复用户级视图。",
331
+ "panel.workspaceClear": "清除工作区",
423
332
  "market.addPlaceholder": "owner/repo 或 https://github.com/…",
424
333
  "market.addSource": "添加源",
425
334
  "market.noSources": "还没有市场源。从下方内置市场添加,或输入一个 GitHub 仓库。",
426
- "market.catalogTitle": "内置市场",
335
+ "market.title": "市场",
427
336
  "market.catalogHint": "精选技能仓库,添加后即可扫描安装,并持续检查更新。",
428
337
  "market.catalog.anthropics": "Anthropic 官方技能合集(Claude Skills)",
429
338
  "market.catalog.superpowers": "Superpowers 社区技能合集",
430
339
  "market.catalog.mattpocock": "Matt Pocock 技能集",
431
340
  "market.catalog.openDesign": "设计技能合集(design-templates)",
432
- "market.added": "已添加",
433
341
  "market.add": "添加",
434
- "market.mySources": "我的市场源",
435
342
  "market.installed": "已装 {count}",
436
343
  "market.updatable": "可更新 {count}",
437
344
  "market.deletedUpstream": "上游已删 {count}",
@@ -496,6 +403,9 @@ window.__ModuleLoader__.load({
496
403
  "groups.empty": "还没有场景。先新建一个,再把技能归类。",
497
404
  "groups.noCollections": "暂无来源组。从市场导入技能后自动按来源聚合。",
498
405
  "groups.personal": "个人",
406
+ "groups.project": "项目级",
407
+ "groups.subdivide": "细分",
408
+ "groups.merge": "合并",
499
409
  "groups.noWritable": "该组没有可写技能(只读技能无法由中枢开关)",
500
410
  "groups.closeAll": "全部关闭",
501
411
  "groups.keepOn": "保留开启",
@@ -522,6 +432,7 @@ window.__ModuleLoader__.load({
522
432
  "row.disable": "禁用",
523
433
  "row.enable": "启用",
524
434
  "row.delete": "移入回收站",
435
+ "row.open": "查看 {name} 详情(回车)",
525
436
  "delete.confirmTitle": "移入回收站?",
526
437
  "delete.confirmText": "确定把「{name}」移入回收站吗?可随时恢复。",
527
438
  "delete.confirm": "移入回收站",
@@ -623,19 +534,19 @@ window.__ModuleLoader__.load({
623
534
  "panel.emptyAll": "No skills installed yet. Create the first one.",
624
535
  "panel.loading": "Loading…",
625
536
  "panel.incomplete": "(catalog may be incomplete)",
626
- "market.addHint": "Codex-style market sources: add a repo (owner/repo or GitHub URL), scan its skills and import; imported skills are tracked upstream automatically.",
537
+ "panel.workspacePlaceholder": "Workspace path (Enter to apply)",
538
+ "panel.workspaceHint": "Enter a project path to also show that workspace's .dsh/skills and .agents/skills skills (read-only); clear + Enter to return to the user-level view.",
539
+ "panel.workspaceClear": "Clear workspace",
627
540
  "market.addPlaceholder": "owner/repo or https://github.com/…",
628
541
  "market.addSource": "Add source",
629
542
  "market.noSources": "No market sources yet. Add one from the built-in catalog below, or enter a GitHub repo.",
630
- "market.catalogTitle": "Market catalog",
543
+ "market.title": "Market",
631
544
  "market.catalogHint": "Curated skill repos — add one to scan and install, with ongoing update checks.",
632
545
  "market.catalog.anthropics": "Official Anthropic skill collection (Claude Skills)",
633
546
  "market.catalog.superpowers": "Superpowers community skill collection",
634
547
  "market.catalog.mattpocock": "Matt Pocock skill collection",
635
548
  "market.catalog.openDesign": "Design skill collection (design-templates)",
636
- "market.added": "Added",
637
549
  "market.add": "Add",
638
- "market.mySources": "My sources",
639
550
  "market.installed": "{count} installed",
640
551
  "market.updatable": "{count} updatable",
641
552
  "market.deletedUpstream": "{count} deleted upstream",
@@ -700,6 +611,9 @@ window.__ModuleLoader__.load({
700
611
  "groups.empty": "No scenes yet. Create one and start categorizing skills.",
701
612
  "groups.noCollections": "No source groups yet. Import skills from the market to group them by source.",
702
613
  "groups.personal": "Personal",
614
+ "groups.project": "Project",
615
+ "groups.subdivide": "Split",
616
+ "groups.merge": "Merge",
703
617
  "groups.noWritable": "No writable skills in this group (read-only skills cannot be switched from the hub)",
704
618
  "groups.closeAll": "Close all",
705
619
  "groups.keepOn": "Keep on",
@@ -726,6 +640,7 @@ window.__ModuleLoader__.load({
726
640
  "row.disable": "Disable",
727
641
  "row.enable": "Enable",
728
642
  "row.delete": "Move to trash",
643
+ "row.open": "View {name} details (Enter)",
729
644
  "delete.confirmTitle": "Move to trash?",
730
645
  "delete.confirmText": "Move \"{name}\" to trash? It can be restored anytime.",
731
646
  "delete.confirm": "Move to trash",
@@ -1301,11 +1216,17 @@ window.__ModuleLoader__.load({
1301
1216
  };
1302
1217
  //#endregion
1303
1218
  //#region src/client/SkillHubSettingsCard.tsx
1219
+ /** Model-invocable dot color default. Single source for the TS side; the
1220
+ * panel's CSS mirrors it via --hub-model (panel.module.css). */
1221
+ const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
1222
+ /** User-invocable dot color default. Single source for the TS side; the
1223
+ * panel's CSS mirrors it via --hub-user (panel.module.css). */
1224
+ const DEFAULT_DOT_USER_COLOR = "#3fb950";
1304
1225
  /** Bridges the hub's config scope onto the card's staged form. */
1305
1226
  var SkillHubSettingsCardController = class {
1306
1227
  form;
1307
1228
  store;
1308
- /** @param scope - the hub config scope the card edits (ApiConfigScope). */
1229
+ /** @param scope - the hub settings scope the card edits (FormScope-compatible). */
1309
1230
  constructor(scope) {
1310
1231
  this.form = new CardForm(scope, [
1311
1232
  booleanField("enabled"),
@@ -1512,15 +1433,23 @@ window.__ModuleLoader__.load({
1512
1433
  }
1513
1434
  /** Origin-repo filter value: skills with no source record (private skills). */
1514
1435
  const PRIVATE_SOURCE = "private";
1436
+ /** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
1437
+ function isProjectSource(source) {
1438
+ return source === "project-dsh" || source === "project-agents";
1439
+ }
1515
1440
  /**
1516
1441
  * Apply the origin filter ('all' or a specific origin repo; skills without a
1517
1442
  * source record count as PRIVATE_SOURCE). The origins map is the store's
1518
1443
  * skillName → repo derivation, so filtering follows the tracked source
1519
1444
  * records instead of the filesystem root a skill happens to live under.
1445
+ * 项目级技能(有 workspace 归属)永远不算「个人」。
1520
1446
  */
1521
1447
  function filterBySource(skills, source, origins) {
1522
1448
  if (source === "all") return [...skills];
1523
- return skills.filter((skill) => (origins[skill.name] ?? "private") === source);
1449
+ return skills.filter((skill) => {
1450
+ if (isProjectSource(skill.source)) return false;
1451
+ return (origins[skill.name] ?? "private") === source;
1452
+ });
1524
1453
  }
1525
1454
  /**
1526
1455
  * Sort a skill list in place-safe copy order: name ascending, added
@@ -1583,7 +1512,7 @@ window.__ModuleLoader__.load({
1583
1512
  }
1584
1513
  //#endregion
1585
1514
  //#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/panel/panel.module.css.mjs
1586
- const css = "._6VtqdG_panel{-webkit-font-smoothing:antialiased;color-scheme:light dark;--hub-model:#2f81f7;--hub-user:#3fb950;flex-direction:column;gap:14px;max-width:720px;margin:0 auto;padding:28px 20px 44px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,sans-serif;display:flex}._6VtqdG_header{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}._6VtqdG_title{letter-spacing:-.02em;margin:0;font-size:24px;font-weight:700}._6VtqdG_hint{opacity:.5;font-size:12px}._6VtqdG_actions{gap:8px;margin-left:auto;display:flex}._6VtqdG_segmented{background:#80808024;border-radius:9px;gap:2px;padding:2px;display:flex}._6VtqdG_segBtn{color:inherit;opacity:.62;cursor:pointer;background:0 0;border:none;border-radius:7px;padding:5px 13px;font-size:12px;transition:background .15s,opacity .15s}._6VtqdG_segBtn:hover{opacity:.9}._6VtqdG_segBtnActive{opacity:1;background:#80808047;font-weight:600}._6VtqdG_search{width:100%;color:inherit;background:#8080801f;border:none;border-radius:10px;outline:none;padding:10px 14px;font-size:13px;transition:background .15s}._6VtqdG_search::placeholder{opacity:.45}._6VtqdG_search:focus{background:#8080802e}._6VtqdG_section{background:#8080800f;border-radius:12px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_sectionTitle{margin:14px 16px 2px;font-size:13px;font-weight:700}._6VtqdG_groupHead{align-items:center;gap:8px;padding:12px 14px 4px;display:flex}._6VtqdG_groupTitle{opacity:.92;flex:1;min-width:0;font-size:13px;font-weight:600}._6VtqdG_groupOps{flex:none;gap:6px;display:inline-flex}._6VtqdG_opBtn{color:inherit;cursor:pointer;background:#80808021;border:none;border-radius:999px;padding:3px 11px;font-size:11px;transition:background .15s}._6VtqdG_opBtn:hover{background:#80808038}._6VtqdG_opBtn:disabled{opacity:.4;cursor:default}._6VtqdG_row{cursor:pointer;background:0 0;align-items:center;gap:12px;padding:10px 14px;transition:background .12s;display:flex}._6VtqdG_row:hover{background:#80808014}._6VtqdG_rowStatic{cursor:default}._6VtqdG_rowStatic:hover{background:0 0}._6VtqdG_row+._6VtqdG_row{border-top:.5px solid #80808021}._6VtqdG_rowMain{flex:1;min-width:0}._6VtqdG_rowName{align-items:center;gap:5px;font-size:13.5px;display:flex}._6VtqdG_rowNameText{font-weight:600}._6VtqdG_rowDesc{opacity:.52;white-space:nowrap;text-overflow:ellipsis;margin-top:1px;font-size:12.5px;overflow:hidden}._6VtqdG_rowMeta{opacity:.45;white-space:nowrap;flex:none;font-size:11.5px}._6VtqdG_badges{flex:none;align-items:center;gap:6px;display:flex}._6VtqdG_badge{opacity:.8;white-space:nowrap;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_badgeModel{color:var(--hub-model);border-color:currentColor}._6VtqdG_badgeUser{color:var(--hub-user);border-color:currentColor}._6VtqdG_badgeUses{color:#d29922;border-color:currentColor}._6VtqdG_badgeReadonly{opacity:.5}._6VtqdG_switch{cursor:pointer;background:#8080804d;border:none;border-radius:999px;flex:none;width:36px;height:21px;padding:2px;transition:background .18s}._6VtqdG_switch:disabled{opacity:.5;cursor:default}._6VtqdG_switchOn{background:#34c759}._6VtqdG_switchThumb{background:#fff;border-radius:50%;width:17px;height:17px;transition:transform .18s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}._6VtqdG_switchOn ._6VtqdG_switchThumb{transform:translate(15px)}._6VtqdG_empty{opacity:.5;text-align:center;padding:20px 0;font-size:13px}._6VtqdG_diagRow{background:#d299220f;border-left:3px solid #d29922;padding:9px 14px;font-size:12px}._6VtqdG_diagPath{opacity:.75;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._6VtqdG_diagReason{opacity:.65;margin-top:1px}._6VtqdG_updateLink{color:inherit;font-weight:600;text-decoration:underline}._6VtqdG_errorBanner{color:#d1242f;white-space:pre-wrap;word-break:break-all;background:#d1242f12;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_successBanner{color:#3fb950;white-space:pre-wrap;word-break:break-all;background:#3fb95014;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_detailHead{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._6VtqdG_back{color:inherit;cursor:pointer;background:#8080801f;border:none;border-radius:8px;padding:6px 12px;font-size:12.5px}._6VtqdG_back:hover{background:#80808033}._6VtqdG_detailName{letter-spacing:-.01em;font-size:19px;font-weight:700}._6VtqdG_detailMeta{opacity:.6;flex-direction:column;gap:4px;font-size:12px;display:flex}._6VtqdG_detailMetaLine{word-break:break-all}._6VtqdG_detailContent{white-space:pre-wrap;word-break:break-word;background:#8080800d;border-radius:12px;max-height:62vh;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;overflow:auto}._6VtqdG_form{background:#8080800d;border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_formRow{flex-direction:column;gap:5px;display:flex}._6VtqdG_formLabel{opacity:.6;font-size:12px}._6VtqdG_input,._6VtqdG_select{color:inherit;background:#8080801f;border:none;border-radius:9px;outline:none;padding:9px 12px;font-size:13px}._6VtqdG_input:focus,._6VtqdG_select:focus{background:#8080802e}._6VtqdG_buttons{align-items:center;gap:8px;display:flex}._6VtqdG_button{cursor:pointer;color:inherit;background:#8080801f;border:none;border-radius:8px;padding:6px 13px;font-size:12.5px;transition:background .15s}._6VtqdG_button:hover{background:#80808033}._6VtqdG_primary{background:var(--hub-model);color:#fff}._6VtqdG_primary:hover{opacity:.92;background:#2f81f7}._6VtqdG_primary:disabled{opacity:.45;cursor:default}._6VtqdG_danger{color:#fff;background:#d1242f}._6VtqdG_danger:hover{opacity:.92;background:#d1242f}._6VtqdG_danger:disabled{opacity:.45;cursor:default}._6VtqdG_formError{color:#d1242f}._6VtqdG_formSuccess{color:#3fb950}._6VtqdG_muted{opacity:.55}._6VtqdG_dot{vertical-align:middle;border-radius:50%;width:7px;height:7px;margin-left:5px;display:inline-block}._6VtqdG_dotModel{background:var(--hub-model)}._6VtqdG_dotUser{background:var(--hub-user)}._6VtqdG_legend{opacity:.55;flex-wrap:wrap;align-items:center;gap:12px;padding:2px 2px 0;font-size:11.5px;display:flex}._6VtqdG_legendItem{align-items:center;gap:4px;display:inline-flex}._6VtqdG_legendItem ._6VtqdG_dot{margin-left:0}._6VtqdG_legendHint{opacity:.8}._6VtqdG_badgeSource,._6VtqdG_badgeCount{opacity:.72;background:#8080801a;border:none}._6VtqdG_badgeDisabled{opacity:.58;background:#8080801a;border:none}._6VtqdG_disclosure{min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:7px;padding:2px 0;display:flex}._6VtqdG_disclosure:hover ._6VtqdG_groupTitle{opacity:1}._6VtqdG_chevron{opacity:.55;border-bottom:1.5px solid;border-right:1.5px solid;flex:none;width:8px;height:8px;margin-right:2px;transition:transform .15s;transform:rotate(45deg)}._6VtqdG_chevronCollapsed{transform:rotate(-45deg)}._6VtqdG_headerCount{opacity:.5;white-space:nowrap;margin-left:2px;font-size:12px}._6VtqdG_subbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_legendToggle{width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:#80808024;border:none;border-radius:50%;flex:none;font-size:12px;line-height:1;transition:opacity .15s,background .15s}._6VtqdG_legendToggle:hover{opacity:1}._6VtqdG_legendToggleActive{opacity:1;background:#80808047}._6VtqdG_filterBar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_formLabel{margin:0}._6VtqdG_useCount{color:#d29922;vertical-align:1px;background:#d299221f;border-radius:999px;margin-left:6px;padding:0 6px;font-size:11px;font-weight:700;line-height:1.6;display:inline-block}._6VtqdG_useTime{opacity:.45;white-space:nowrap;margin-left:auto;font-size:11px}._6VtqdG_switchMixed{background:#2f81f773}._6VtqdG_switchMixed ._6VtqdG_switchThumb{transform:translate(7.5px);box-shadow:0 1px 2px #00000040}._6VtqdG_groupTitleInner{align-items:baseline;display:inline-flex}._6VtqdG_groupOps{flex-wrap:wrap;flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_sourceLink{color:inherit;opacity:.9;border-bottom:1px dotted;font-weight:600;text-decoration:none}._6VtqdG_sourceLink:hover{opacity:1}._6VtqdG_statusBadges{flex:none;align-items:center;gap:4px;display:inline-flex}._6VtqdG_statusBadge{opacity:.8;white-space:nowrap;background:#8080801a;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_statusOk{color:#3fb950;background:0 0;border-color:currentColor}._6VtqdG_statusUpdated{color:#d29922;background:#d2992214;border-color:currentColor}._6VtqdG_statusError{color:#d1242f;background:#d1242f14;border-color:currentColor}._6VtqdG_statusWrap{cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-family:inherit;display:inline-flex}._6VtqdG_statusWrap:hover ._6VtqdG_statusBadge{opacity:1}._6VtqdG_statusButton{cursor:pointer;font-family:inherit}._6VtqdG_statusButton:hover{opacity:1}._6VtqdG_opDanger{color:#d1242f;background:#d1242f1a}._6VtqdG_opDanger:hover{background:#d1242f2e}._6VtqdG_iconBtn{border-radius:999px;justify-content:center;align-items:center;width:24px;height:24px;padding:0;display:inline-flex}._6VtqdG_sourceCard{background:#8080800d;border-radius:12px;flex-direction:column;gap:4px;padding:12px 14px;font-size:12px;display:flex}._6VtqdG_sourceCardTitle{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_dialogOverlay{z-index:50;-webkit-backdrop-filter:blur(2px);background:#00000059;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}._6VtqdG_dialog{background:var(--dsw-surface,#f5f5f7);min-width:min(320px,100%);max-width:380px;color:var(--dsw-text,#1d1d1f);border-radius:14px;padding:18px 18px 14px;box-shadow:0 12px 40px #00000047,0 2px 8px #00000029}@media (prefers-color-scheme:dark){._6VtqdG_dialog{color:#f5f5f7;background:#2c2c2e}}._6VtqdG_dialogTitle{letter-spacing:-.01em;margin:0 0 8px;font-size:15px;font-weight:700}._6VtqdG_dialogText{opacity:.65;margin:0 0 10px;font-size:12.5px;line-height:1.5}._6VtqdG_dialogList{flex-direction:column;gap:5px;max-height:200px;margin:0 0 14px;padding:0;list-style:none;display:flex;overflow:auto}._6VtqdG_dialogList li{opacity:.85;word-break:break-all;font-size:12.5px}._6VtqdG_dialogActions{justify-content:flex-end;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_search{flex:1;min-width:160px}._6VtqdG_filterBar ._6VtqdG_formLabel{flex:none}._6VtqdG_filterBar ._6VtqdG_select{flex:none;max-width:150px}._6VtqdG_filterBar ._6VtqdG_segmented{flex:none}._6VtqdG_titleIcon{vertical-align:-2px;opacity:.8;margin-right:2px;display:inline-block}._6VtqdG_grow{flex:1}._6VtqdG_hintLine{opacity:.55;margin:0;font-size:11.5px}._6VtqdG_hintPadded{margin:6px 12px 2px}._6VtqdG_hintInline{margin:4px 2px}._6VtqdG_rowMuted{cursor:default;opacity:.55}._6VtqdG_actionsPadded{padding:8px 12px}._6VtqdG_actionsTop{margin-top:8px}._6VtqdG_actionsBottom{margin-bottom:8px}._6VtqdG_groupTime{margin-left:6px}._6VtqdG_sectionHeadRow{align-items:center;gap:8px;display:flex}._6VtqdG_sectionTitleFill{flex:1}._6VtqdG_dialogSelect{width:100%;margin-bottom:12px}._6VtqdG_dialogRow{align-items:center;gap:8px;display:flex}";
1515
+ const css = "._6VtqdG_panel{-webkit-font-smoothing:antialiased;color-scheme:light dark;--hub-model:#2f81f7;--hub-user:#3fb950;flex-direction:column;gap:14px;max-width:720px;margin:0 auto;padding:28px 20px 44px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,sans-serif;display:flex}._6VtqdG_header{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}._6VtqdG_title{letter-spacing:-.02em;margin:0;font-size:24px;font-weight:700}._6VtqdG_hint{opacity:.5;font-size:12px}._6VtqdG_actions{gap:8px;margin-left:auto;display:flex}._6VtqdG_segmented{background:#80808024;border-radius:9px;gap:2px;padding:2px;display:flex}._6VtqdG_segBtn{color:inherit;opacity:.62;cursor:pointer;background:0 0;border:none;border-radius:7px;padding:5px 13px;font-size:12px;transition:background .15s,opacity .15s}._6VtqdG_segBtn:hover{opacity:.9}._6VtqdG_segBtnActive{opacity:1;background:#80808047;font-weight:600}._6VtqdG_search{width:100%;color:inherit;background:#8080801f;border:none;border-radius:10px;outline:none;padding:10px 14px;font-size:13px;transition:background .15s}._6VtqdG_search::placeholder{opacity:.45}._6VtqdG_search:focus{background:#8080802e}._6VtqdG_section{background:#8080800f;border-radius:12px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_sectionTitle{margin:14px 16px 2px;font-size:13px;font-weight:700}._6VtqdG_groupHead{align-items:center;gap:8px;padding:12px 14px 4px;display:flex}._6VtqdG_groupTitle{opacity:.92;flex:1;min-width:0;font-size:13px;font-weight:600}._6VtqdG_groupOps{flex:none;gap:6px;display:inline-flex}._6VtqdG_opBtn{color:inherit;cursor:pointer;background:#80808021;border:none;border-radius:999px;padding:3px 11px;font-size:11px;transition:background .15s}._6VtqdG_opBtn:hover{background:#80808038}._6VtqdG_opBtn:disabled{opacity:.4;cursor:default}._6VtqdG_row{cursor:pointer;background:0 0;align-items:center;gap:12px;padding:10px 14px;transition:background .12s;display:flex}._6VtqdG_row:hover{background:#80808014}._6VtqdG_row:focus-visible{outline:2px solid var(--hub-model);outline-offset:-2px}._6VtqdG_rowStatic{cursor:default}._6VtqdG_rowStatic:hover{background:0 0}._6VtqdG_row+._6VtqdG_row{border-top:.5px solid #80808021}._6VtqdG_rowMain{flex:1;min-width:0}._6VtqdG_rowName{align-items:center;gap:5px;font-size:13.5px;display:flex}._6VtqdG_rowNameText{font-weight:600}._6VtqdG_rowDesc{opacity:.52;white-space:nowrap;text-overflow:ellipsis;margin-top:1px;font-size:12.5px;overflow:hidden}._6VtqdG_rowMeta{opacity:.45;white-space:nowrap;flex:none;font-size:11.5px}._6VtqdG_badges{flex:none;align-items:center;gap:6px;display:flex}._6VtqdG_badge{opacity:.8;white-space:nowrap;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_badgeModel{color:var(--hub-model);border-color:currentColor}._6VtqdG_badgeUser{color:var(--hub-user);border-color:currentColor}._6VtqdG_badgeUses{color:#d29922;border-color:currentColor}._6VtqdG_badgeReadonly{opacity:.5}._6VtqdG_switch{cursor:pointer;background:#8080804d;border:none;border-radius:999px;flex:none;width:36px;height:21px;padding:2px;transition:background .18s}._6VtqdG_switch:disabled{opacity:.5;cursor:default}._6VtqdG_switchOn{background:#34c759}._6VtqdG_switchThumb{background:#fff;border-radius:50%;width:17px;height:17px;transition:transform .18s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}._6VtqdG_switchOn ._6VtqdG_switchThumb{transform:translate(15px)}._6VtqdG_empty{opacity:.5;text-align:center;padding:20px 0;font-size:13px}._6VtqdG_diagRow{background:#d299220f;border-left:3px solid #d29922;padding:9px 14px;font-size:12px}._6VtqdG_diagPath{opacity:.75;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._6VtqdG_diagReason{opacity:.65;margin-top:1px}._6VtqdG_updateLink{color:inherit;font-weight:600;text-decoration:underline}._6VtqdG_errorBanner{color:#d1242f;white-space:pre-wrap;word-break:break-all;background:#d1242f12;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_successBanner{color:#3fb950;white-space:pre-wrap;word-break:break-all;background:#3fb95014;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_detailHead{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._6VtqdG_back{color:inherit;cursor:pointer;background:#8080801f;border:none;border-radius:8px;padding:6px 12px;font-size:12.5px}._6VtqdG_back:hover{background:#80808033}._6VtqdG_detailName{letter-spacing:-.01em;font-size:19px;font-weight:700}._6VtqdG_detailMeta{opacity:.6;flex-direction:column;gap:4px;font-size:12px;display:flex}._6VtqdG_detailMetaLine{word-break:break-all}._6VtqdG_detailContent{white-space:pre-wrap;word-break:break-word;background:#8080800d;border-radius:12px;max-height:62vh;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;overflow:auto}._6VtqdG_form{background:#8080800d;border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_formRow{flex-direction:column;gap:5px;display:flex}._6VtqdG_formLabel{opacity:.6;font-size:12px}._6VtqdG_input,._6VtqdG_select{color:inherit;background:#8080801f;border:none;border-radius:9px;outline:none;padding:9px 12px;font-size:13px}._6VtqdG_input:focus,._6VtqdG_select:focus{background:#8080802e}._6VtqdG_buttons{align-items:center;gap:8px;display:flex}._6VtqdG_button{cursor:pointer;color:inherit;background:#8080801f;border:none;border-radius:8px;padding:6px 13px;font-size:12.5px;transition:background .15s}._6VtqdG_button:hover{background:#80808033}._6VtqdG_primary{background:var(--hub-model);color:#fff}._6VtqdG_primary:hover{opacity:.92;background:#2f81f7}._6VtqdG_primary:disabled{opacity:.45;cursor:default}._6VtqdG_danger{color:#fff;background:#d1242f}._6VtqdG_danger:hover{opacity:.92;background:#d1242f}._6VtqdG_danger:disabled{opacity:.45;cursor:default}._6VtqdG_formError{color:#d1242f}._6VtqdG_formSuccess{color:#3fb950}._6VtqdG_muted{opacity:.55}._6VtqdG_dot{vertical-align:middle;border-radius:50%;width:7px;height:7px;margin-left:5px;display:inline-block}._6VtqdG_dotModel{background:var(--hub-model)}._6VtqdG_dotUser{background:var(--hub-user)}._6VtqdG_legend{opacity:.55;flex-wrap:wrap;align-items:center;gap:12px;padding:2px 2px 0;font-size:11.5px;display:flex}._6VtqdG_legendItem{align-items:center;gap:4px;display:inline-flex}._6VtqdG_legendItem ._6VtqdG_dot{margin-left:0}._6VtqdG_legendHint{opacity:.8}._6VtqdG_badgeSource,._6VtqdG_badgeCount{opacity:.72;background:#8080801a;border:none}._6VtqdG_badgeDisabled{opacity:.58;background:#8080801a;border:none}._6VtqdG_disclosure{min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:7px;padding:2px 0;display:flex}._6VtqdG_disclosure:hover ._6VtqdG_groupTitle{opacity:1}._6VtqdG_chevron{opacity:.55;border-bottom:1.5px solid;border-right:1.5px solid;flex:none;width:8px;height:8px;margin-right:2px;transition:transform .15s;transform:rotate(45deg)}._6VtqdG_chevronCollapsed{transform:rotate(-45deg)}._6VtqdG_headerCount{opacity:.5;white-space:nowrap;margin-left:2px;font-size:12px}._6VtqdG_subbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_legendToggle{width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:#80808024;border:none;border-radius:50%;flex:none;font-size:12px;line-height:1;transition:opacity .15s,background .15s}._6VtqdG_legendToggle:hover{opacity:1}._6VtqdG_legendToggleActive{opacity:1;background:#80808047}._6VtqdG_filterBar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_formLabel{margin:0}._6VtqdG_useCount{color:#d29922;vertical-align:1px;background:#d299221f;border-radius:999px;margin-left:6px;padding:0 6px;font-size:11px;font-weight:700;line-height:1.6;display:inline-block}._6VtqdG_useTime{opacity:.45;white-space:nowrap;margin-left:auto;font-size:11px}._6VtqdG_switchMixed{background:#2f81f773}._6VtqdG_switchMixed ._6VtqdG_switchThumb{transform:translate(7.5px);box-shadow:0 1px 2px #00000040}._6VtqdG_groupTitleInner{align-items:baseline;display:inline-flex}._6VtqdG_groupOps{flex-wrap:wrap;flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_sourceLink{color:inherit;opacity:.9;border-bottom:1px dotted;font-weight:600;text-decoration:none}._6VtqdG_sourceLink:hover{opacity:1}._6VtqdG_statusBadges{flex:none;align-items:center;gap:4px;display:inline-flex}._6VtqdG_statusBadge{opacity:.8;white-space:nowrap;background:#8080801a;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_statusOk{color:#3fb950;background:0 0;border-color:currentColor}._6VtqdG_statusUpdated{color:#d29922;background:#d2992214;border-color:currentColor}._6VtqdG_statusError{color:#d1242f;background:#d1242f14;border-color:currentColor}._6VtqdG_statusWrap{cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-family:inherit;display:inline-flex}._6VtqdG_statusWrap:hover ._6VtqdG_statusBadge{opacity:1}._6VtqdG_statusButton{cursor:pointer;font-family:inherit}._6VtqdG_statusButton:hover{opacity:1}._6VtqdG_opDanger{color:#d1242f;background:#d1242f1a}._6VtqdG_opDanger:hover{background:#d1242f2e}._6VtqdG_iconBtn{border-radius:999px;justify-content:center;align-items:center;width:24px;height:24px;padding:0;display:inline-flex}._6VtqdG_sourceCard{background:#8080800d;border-radius:12px;flex-direction:column;gap:4px;padding:12px 14px;font-size:12px;display:flex}._6VtqdG_sourceCardTitle{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_dialogOverlay{z-index:50;-webkit-backdrop-filter:blur(2px);background:#00000059;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}._6VtqdG_dialog{background:var(--dsw-surface,#f5f5f7);min-width:min(320px,100%);max-width:380px;color:var(--dsw-text,#1d1d1f);border-radius:14px;padding:18px 18px 14px;box-shadow:0 12px 40px #00000047,0 2px 8px #00000029}@media (prefers-color-scheme:dark){._6VtqdG_dialog{color:#f5f5f7;background:#2c2c2e}}._6VtqdG_dialogTitle{letter-spacing:-.01em;margin:0 0 8px;font-size:15px;font-weight:700}._6VtqdG_dialogText{opacity:.65;margin:0 0 10px;font-size:12.5px;line-height:1.5}._6VtqdG_dialogList{flex-direction:column;gap:5px;max-height:200px;margin:0 0 14px;padding:0;list-style:none;display:flex;overflow:auto}._6VtqdG_dialogList li{opacity:.85;word-break:break-all;font-size:12.5px}._6VtqdG_dialogActions{justify-content:flex-end;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_search{flex:1;min-width:160px}._6VtqdG_filterBar ._6VtqdG_formLabel{flex:none}._6VtqdG_filterBar ._6VtqdG_select{flex:none;max-width:150px}._6VtqdG_filterBar ._6VtqdG_segmented{flex:none}._6VtqdG_titleIcon{vertical-align:-2px;opacity:.8;margin-right:2px;display:inline-block}._6VtqdG_grow{flex:1}._6VtqdG_hintLine{opacity:.55;margin:0;font-size:11.5px}._6VtqdG_hintPadded{margin:6px 12px 2px}._6VtqdG_hintInline{margin:4px 2px}._6VtqdG_rowMuted{cursor:default;opacity:.55}._6VtqdG_actionsPadded{padding:8px 12px}._6VtqdG_actionsTop{margin-top:8px}._6VtqdG_actionsBottom{margin-bottom:8px}._6VtqdG_groupTime{margin-left:6px}._6VtqdG_sectionHeadRow{align-items:center;gap:8px;display:flex}._6VtqdG_sectionTitleFill{flex:1}._6VtqdG_dialogSelect{width:100%;margin-bottom:12px}._6VtqdG_dialogRow{align-items:center;gap:8px;display:flex}._6VtqdG_workspaceBox{align-items:center;gap:6px;display:inline-flex}._6VtqdG_workspaceInput{width:200px;padding:6px 10px;font-size:12px}._6VtqdG_projectNest{border-left:1px solid #80808024;flex-direction:column;margin:2px 0 0 14px;display:flex}";
1587
1516
  const tagId = "dsh-skill-hub/panel.module.css";
1588
1517
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1589
1518
  const tag = document.createElement("style");
@@ -1664,6 +1593,7 @@ window.__ModuleLoader__.load({
1664
1593
  "opDanger": "_6VtqdG_opDanger",
1665
1594
  "panel": "_6VtqdG_panel",
1666
1595
  "primary": "_6VtqdG_primary",
1596
+ "projectNest": "_6VtqdG_projectNest",
1667
1597
  "row": "_6VtqdG_row",
1668
1598
  "rowDesc": "_6VtqdG_rowDesc",
1669
1599
  "rowMain": "_6VtqdG_rowMain",
@@ -1701,7 +1631,9 @@ window.__ModuleLoader__.load({
1701
1631
  "titleIcon": "_6VtqdG_titleIcon",
1702
1632
  "updateLink": "_6VtqdG_updateLink",
1703
1633
  "useCount": "_6VtqdG_useCount",
1704
- "useTime": "_6VtqdG_useTime"
1634
+ "useTime": "_6VtqdG_useTime",
1635
+ "workspaceBox": "_6VtqdG_workspaceBox",
1636
+ "workspaceInput": "_6VtqdG_workspaceInput"
1705
1637
  };
1706
1638
  //#endregion
1707
1639
  //#region src/client/panel/SourceStatusBadge.tsx
@@ -2055,11 +1987,23 @@ window.__ModuleLoader__.load({
2055
1987
  const stat = hub.uses.get(skill.name);
2056
1988
  const count = stat?.count ?? 0;
2057
1989
  const lastUsed = stat?.lastUsed;
1990
+ /** 键盘打开详情:Enter 或空格。仅当焦点在行本身(而非行内按钮)时生效。 */
1991
+ const onKeyDown = (event) => {
1992
+ if (event.target !== event.currentTarget) return;
1993
+ if (event.key === "Enter" || event.key === " ") {
1994
+ event.preventDefault();
1995
+ hub.openDetail(skill.name);
1996
+ }
1997
+ };
2058
1998
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2059
1999
  className: panel_module_css_default.row,
2000
+ role: "button",
2001
+ tabIndex: 0,
2002
+ "aria-label": tt("row.open", { name: skill.name }),
2060
2003
  onClick: () => {
2061
2004
  hub.openDetail(skill.name);
2062
2005
  },
2006
+ onKeyDown,
2063
2007
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2064
2008
  className: panel_module_css_default.rowMain,
2065
2009
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2187,6 +2131,123 @@ window.__ModuleLoader__.load({
2187
2131
  }
2188
2132
  //#endregion
2189
2133
  //#region src/client/panel/SourcesView.tsx
2134
+ /** 项目级三级树:项目级 → 具体工作区(可折叠、可细分)→ .dsh/.agents。 */
2135
+ function ProjectTree(props) {
2136
+ const { hub } = props;
2137
+ const { sorted, sourceFilter, origins, collapsedGroups, subdividedProjects, toggleGroupCollapse, toggleSubdivide } = hub;
2138
+ const projectSkills = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
2139
+ if (projectSkills.length === 0) return null;
2140
+ const byProject = /* @__PURE__ */ new Map();
2141
+ for (const skill of projectSkills) {
2142
+ const key = skill.workspace ?? skill.source;
2143
+ const entry = byProject.get(key);
2144
+ if (entry === void 0) byProject.set(key, {
2145
+ title: skill.workspaceTitle ?? skill.workspace ?? tt("groups.project"),
2146
+ skills: [skill]
2147
+ });
2148
+ else entry.skills.push(skill);
2149
+ }
2150
+ const topCollapsed = collapsedGroups.has("project");
2151
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2152
+ className: panel_module_css_default.section,
2153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2154
+ className: panel_module_css_default.groupHead,
2155
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2156
+ type: "button",
2157
+ className: panel_module_css_default.disclosure,
2158
+ "aria-expanded": !topCollapsed,
2159
+ onClick: () => {
2160
+ toggleGroupCollapse("project");
2161
+ },
2162
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (topCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2163
+ className: panel_module_css_default.groupTitle,
2164
+ children: [
2165
+ tt("groups.project"),
2166
+ " · ",
2167
+ byProject.size
2168
+ ]
2169
+ })]
2170
+ })
2171
+ }), !topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
2172
+ const projKey = "project:" + key;
2173
+ const projCollapsed = collapsedGroups.has(projKey);
2174
+ const subdivided = subdividedProjects.has(key);
2175
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2176
+ className: panel_module_css_default.projectNest,
2177
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2178
+ className: panel_module_css_default.groupHead,
2179
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2180
+ type: "button",
2181
+ className: panel_module_css_default.disclosure,
2182
+ "aria-expanded": !projCollapsed,
2183
+ onClick: () => {
2184
+ toggleGroupCollapse(projKey);
2185
+ },
2186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (projCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2187
+ className: panel_module_css_default.groupTitle,
2188
+ children: [
2189
+ proj.title,
2190
+ " · ",
2191
+ proj.skills.length,
2192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2193
+ members: proj.skills.map((skill) => skill.name),
2194
+ hub
2195
+ })
2196
+ ]
2197
+ })]
2198
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2199
+ className: panel_module_css_default.groupOps,
2200
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2201
+ type: "button",
2202
+ className: panel_module_css_default.opBtn,
2203
+ onClick: (event) => {
2204
+ event.stopPropagation();
2205
+ toggleSubdivide(key);
2206
+ },
2207
+ children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2208
+ })
2209
+ })]
2210
+ }), !projCollapsed ? subdivided ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2211
+ className: panel_module_css_default.projectNest,
2212
+ children: ["project-dsh", "project-agents"].map((source) => {
2213
+ const list = proj.skills.filter((skill) => skill.source === source);
2214
+ if (list.length === 0) return null;
2215
+ const srcKey = projKey + ":" + source;
2216
+ const srcCollapsed = collapsedGroups.has(srcKey);
2217
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2218
+ className: panel_module_css_default.projectNest,
2219
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2220
+ className: panel_module_css_default.groupHead,
2221
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2222
+ type: "button",
2223
+ className: panel_module_css_default.disclosure,
2224
+ "aria-expanded": !srcCollapsed,
2225
+ onClick: () => {
2226
+ toggleGroupCollapse(srcKey);
2227
+ },
2228
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (srcCollapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2229
+ className: panel_module_css_default.groupTitle,
2230
+ children: [
2231
+ tt("badge.source." + source),
2232
+ " · ",
2233
+ list.length
2234
+ ]
2235
+ })]
2236
+ })
2237
+ }), !srcCollapsed ? list.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2238
+ skill,
2239
+ hub
2240
+ }, skill.name)) : null]
2241
+ }, srcKey);
2242
+ })
2243
+ }) : proj.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2244
+ skill,
2245
+ hub
2246
+ }, skill.name)) : null]
2247
+ }, projKey);
2248
+ }) : null]
2249
+ });
2250
+ }
2190
2251
  function SourcesView(props) {
2191
2252
  const { hub } = props;
2192
2253
  const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, toggleGroup, enableDisabled } = hub;
@@ -2195,6 +2256,7 @@ window.__ModuleLoader__.load({
2195
2256
  hub
2196
2257
  }, skill.name)) });
2197
2258
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTree, { hub }),
2198
2260
  groupsState !== null && groupsState.collections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2199
2261
  className: panel_module_css_default.empty,
2200
2262
  children: tt("groups.noCollections")
@@ -2295,7 +2357,7 @@ window.__ModuleLoader__.load({
2295
2357
  }, "col:" + collection.name);
2296
2358
  }),
2297
2359
  (() => {
2298
- const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0);
2360
+ const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
2299
2361
  if (uncategorized.length === 0) return null;
2300
2362
  const collapsed = collapsedGroups.has("uncategorized-source");
2301
2363
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -2658,6 +2720,12 @@ window.__ModuleLoader__.load({
2658
2720
  }
2659
2721
  //#endregion
2660
2722
  //#region src/client/panel/MarketView.tsx
2723
+ /**
2724
+ * Market tab: one unified market list — built-in catalog entries (add
2725
+ * button while not yet added) and the user's market sources (full state
2726
+ * badges + actions once added, plus custom sources), with check-all and
2727
+ * update-all actions and the repo scan result with the importable checklist.
2728
+ */
2661
2729
  /** Compact byte size for repo preview rows. */
2662
2730
  function formatBytes(bytes) {
2663
2731
  if (bytes < 1024) return bytes + " B";
@@ -2669,54 +2737,140 @@ window.__ModuleLoader__.load({
2669
2737
  const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
2670
2738
  /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
2671
2739
  const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
2672
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2673
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2674
- className: panel_module_css_default.section,
2740
+ /** 一个已添加来源的完整行:状态徽章 + 操作按钮。 */
2741
+ const sourceRow = (record) => {
2742
+ const releaseCheck = marketCheck[record.repo];
2743
+ const skillCheck = sourceCheck[record.repo];
2744
+ const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
2745
+ const scanning = scanningRepo === record.repo;
2746
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2747
+ className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2675
2748
  children: [
2676
2749
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2677
- className: panel_module_css_default.sectionTitle,
2678
- children: tt("market.catalogTitle")
2750
+ className: panel_module_css_default.rowMain,
2751
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2752
+ className: panel_module_css_default.rowName,
2753
+ children: [
2754
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2755
+ className: panel_module_css_default.sourceLink,
2756
+ href: "https://github.com/" + record.repo,
2757
+ target: "_blank",
2758
+ rel: "noreferrer",
2759
+ children: record.repo
2760
+ }),
2761
+ record.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2762
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
2763
+ children: record.ref
2764
+ }) : null,
2765
+ installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2766
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
2767
+ children: tt("market.installed", { count: installedCount })
2768
+ }) : null,
2769
+ skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2770
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2771
+ children: tt("market.updatable", { count: skillCheck.updated.length })
2772
+ }) : null,
2773
+ skillCheck !== void 0 && skillCheck.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2774
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
2775
+ children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
2776
+ }) : null,
2777
+ releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2778
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2779
+ children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
2780
+ }) : null
2781
+ ]
2782
+ })
2679
2783
  }),
2680
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2681
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
2682
- children: tt("market.catalogHint")
2784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2785
+ type: "button",
2786
+ className: panel_module_css_default.opBtn,
2787
+ disabled: scanning,
2788
+ onClick: () => {
2789
+ checkMarket();
2790
+ },
2791
+ children: tt("market.check")
2683
2792
  }),
2684
- MARKET_CATALOG.map((entry) => {
2685
- const added = marketState.repos.some((record) => record.repo === entry.repo);
2686
- const busy = scanningRepo !== null || tagBusy;
2687
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2688
- className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2689
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2690
- className: panel_module_css_default.rowMain,
2691
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2692
- className: panel_module_css_default.rowName,
2693
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2694
- className: panel_module_css_default.sourceLink,
2695
- href: "https://github.com/" + entry.repo,
2696
- target: "_blank",
2697
- rel: "noreferrer",
2698
- children: entry.repo
2699
- })
2700
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2701
- className: panel_module_css_default.rowDesc,
2702
- children: tt(entry.descriptionKey)
2703
- })]
2704
- }), added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2705
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
2706
- children: tt("market.added")
2707
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2708
- type: "button",
2709
- className: panel_module_css_default.opBtn,
2710
- disabled: busy,
2711
- onClick: () => {
2712
- addSource(entry.repo);
2713
- },
2714
- children: tt("market.add")
2715
- })]
2716
- }, entry.repo);
2793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2794
+ type: "button",
2795
+ className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
2796
+ disabled: syncingMarket === record.repo,
2797
+ onClick: () => {
2798
+ syncMarketSource(record.repo);
2799
+ },
2800
+ children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
2801
+ }),
2802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2803
+ type: "button",
2804
+ className: panel_module_css_default.opBtn,
2805
+ disabled: scanning,
2806
+ onClick: () => {
2807
+ scanRepo(record.repo);
2808
+ },
2809
+ children: scanning ? tt("market.scanning") : tt("market.scan")
2810
+ }),
2811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2812
+ type: "button",
2813
+ className: panel_module_css_default.opBtn,
2814
+ disabled: tagBusy,
2815
+ title: tt("market.removeHint"),
2816
+ onClick: () => {
2817
+ removeMarketSource(record.repo);
2818
+ },
2819
+ children: tt("market.deleteSource")
2717
2820
  })
2718
2821
  ]
2719
- }),
2822
+ });
2823
+ };
2824
+ const rows = [];
2825
+ for (const entry of MARKET_CATALOG) {
2826
+ const record = marketState.repos.find((item) => item.repo === entry.repo);
2827
+ if (record !== void 0) rows.push({
2828
+ key: entry.repo,
2829
+ element: sourceRow(record)
2830
+ });
2831
+ else {
2832
+ const busy = scanningRepo !== null || tagBusy;
2833
+ rows.push({
2834
+ key: entry.repo,
2835
+ element: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2836
+ className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2837
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2838
+ className: panel_module_css_default.rowMain,
2839
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2840
+ className: panel_module_css_default.rowName,
2841
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2842
+ className: panel_module_css_default.sourceLink,
2843
+ href: "https://github.com/" + entry.repo,
2844
+ target: "_blank",
2845
+ rel: "noreferrer",
2846
+ children: entry.repo
2847
+ })
2848
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2849
+ className: panel_module_css_default.rowDesc,
2850
+ children: tt(entry.descriptionKey)
2851
+ })]
2852
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2853
+ type: "button",
2854
+ className: panel_module_css_default.opBtn,
2855
+ disabled: busy,
2856
+ onClick: () => {
2857
+ addSource(entry.repo);
2858
+ },
2859
+ children: tt("market.add")
2860
+ })]
2861
+ })
2862
+ });
2863
+ }
2864
+ }
2865
+ for (const record of marketState.repos) {
2866
+ if (MARKET_CATALOG.some((entry) => entry.repo === record.repo)) continue;
2867
+ rows.push({
2868
+ key: record.repo,
2869
+ element: sourceRow(record)
2870
+ });
2871
+ }
2872
+ const unaddedCatalog = MARKET_CATALOG.filter((entry) => !marketState.repos.some((record) => record.repo === entry.repo));
2873
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2720
2874
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2721
2875
  className: panel_module_css_default.section,
2722
2876
  children: [
@@ -2725,7 +2879,7 @@ window.__ModuleLoader__.load({
2725
2879
  children: [
2726
2880
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2727
2881
  className: panel_module_css_default.sectionTitleFill,
2728
- children: tt("market.mySources")
2882
+ children: tt("market.title")
2729
2883
  }),
2730
2884
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2731
2885
  type: "button",
@@ -2747,10 +2901,6 @@ window.__ModuleLoader__.load({
2747
2901
  })
2748
2902
  ]
2749
2903
  }),
2750
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2751
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
2752
- children: tt("market.addHint")
2753
- }),
2754
2904
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2755
2905
  className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsPadded,
2756
2906
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -2776,97 +2926,19 @@ window.__ModuleLoader__.load({
2776
2926
  children: tt("market.addSource")
2777
2927
  })]
2778
2928
  }),
2929
+ unaddedCatalog.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2930
+ className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
2931
+ children: tt("market.catalogHint")
2932
+ }) : null,
2779
2933
  marketState.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2780
2934
  className: panel_module_css_default.empty,
2781
2935
  children: tt("panel.loading")
2782
2936
  }) : null,
2783
- marketState.status === "ready" && marketState.repos.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2937
+ marketState.status !== "loading" && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2784
2938
  className: panel_module_css_default.empty,
2785
2939
  children: tt("market.noSources")
2786
2940
  }) : null,
2787
- marketState.repos.map((record) => {
2788
- const releaseCheck = marketCheck[record.repo];
2789
- const skillCheck = sourceCheck[record.repo];
2790
- const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
2791
- const scanning = scanningRepo === record.repo;
2792
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2793
- className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2794
- children: [
2795
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2796
- className: panel_module_css_default.rowMain,
2797
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2798
- className: panel_module_css_default.rowName,
2799
- children: [
2800
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2801
- className: panel_module_css_default.sourceLink,
2802
- href: "https://github.com/" + record.repo,
2803
- target: "_blank",
2804
- rel: "noreferrer",
2805
- children: record.repo
2806
- }),
2807
- record.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2808
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
2809
- children: record.ref
2810
- }) : null,
2811
- installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2812
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
2813
- children: tt("market.installed", { count: installedCount })
2814
- }) : null,
2815
- skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2816
- className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2817
- children: tt("market.updatable", { count: skillCheck.updated.length })
2818
- }) : null,
2819
- skillCheck !== void 0 && skillCheck.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2820
- className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
2821
- children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
2822
- }) : null,
2823
- releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2824
- className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2825
- children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
2826
- }) : null
2827
- ]
2828
- })
2829
- }),
2830
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2831
- type: "button",
2832
- className: panel_module_css_default.opBtn,
2833
- disabled: scanning,
2834
- onClick: () => {
2835
- checkMarket();
2836
- },
2837
- children: tt("market.check")
2838
- }),
2839
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2840
- type: "button",
2841
- className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
2842
- disabled: syncingMarket === record.repo,
2843
- onClick: () => {
2844
- syncMarketSource(record.repo);
2845
- },
2846
- children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
2847
- }),
2848
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2849
- type: "button",
2850
- className: panel_module_css_default.opBtn,
2851
- disabled: scanning,
2852
- onClick: () => {
2853
- scanRepo(record.repo);
2854
- },
2855
- children: scanning ? tt("market.scanning") : tt("market.scan")
2856
- }),
2857
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2858
- type: "button",
2859
- className: panel_module_css_default.opBtn,
2860
- disabled: tagBusy,
2861
- title: tt("market.removeHint"),
2862
- onClick: () => {
2863
- removeMarketSource(record.repo);
2864
- },
2865
- children: tt("market.deleteSource")
2866
- })
2867
- ]
2868
- }, record.repo);
2869
- })
2941
+ marketState.status !== "loading" ? rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: row.element }, row.key)) : null
2870
2942
  ]
2871
2943
  }),
2872
2944
  repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3039,6 +3111,8 @@ window.__ModuleLoader__.load({
3039
3111
  const [repoImporting, setRepoImporting] = (0, react.useState)(false);
3040
3112
  const [repoResult, setRepoResult] = (0, react.useState)(null);
3041
3113
  const [search, setSearch] = (0, react.useState)("");
3114
+ /** 工作区(项目)路径;空 = 只看用户级技能。 */
3115
+ const [workspace, setWorkspace] = (0, react.useState)("");
3042
3116
  const [detail, setDetail] = (0, react.useState)(null);
3043
3117
  const [detailLoading, setDetailLoading] = (0, react.useState)(false);
3044
3118
  const [busyNames, setBusyNames] = (0, react.useState)(/* @__PURE__ */ new Set());
@@ -3083,8 +3157,10 @@ window.__ModuleLoader__.load({
3083
3157
  const [newTagName, setNewTagName] = (0, react.useState)("");
3084
3158
  const [tagBusy, setTagBusy] = (0, react.useState)(false);
3085
3159
  const [editSearch, setEditSearch] = (0, react.useState)("");
3086
- /** 分组视图里收起的分组(key 为 tag:<id> 或 col:<name>)。 */
3160
+ /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。 */
3087
3161
  const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
3162
+ /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
3163
+ const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
3088
3164
  const [showLegend, setShowLegend] = (0, react.useState)(false);
3089
3165
  const toggleGroupCollapse = (0, react.useCallback)((key) => {
3090
3166
  setCollapsedGroups((previous) => {
@@ -3094,9 +3170,17 @@ window.__ModuleLoader__.load({
3094
3170
  return next;
3095
3171
  });
3096
3172
  }, []);
3173
+ const toggleSubdivide = (0, react.useCallback)((key) => {
3174
+ setSubdividedProjects((previous) => {
3175
+ const next = new Set(previous);
3176
+ if (next.has(key)) next.delete(key);
3177
+ else next.add(key);
3178
+ return next;
3179
+ });
3180
+ }, []);
3097
3181
  const load = (0, react.useCallback)(async () => {
3098
3182
  try {
3099
- const next = await api.catalog();
3183
+ const next = await api.catalog(workspace !== "" ? { cwd: workspace } : void 0);
3100
3184
  setCatalog(next);
3101
3185
  setLoadError(null);
3102
3186
  } catch (error) {
@@ -3104,7 +3188,7 @@ window.__ModuleLoader__.load({
3104
3188
  } finally {
3105
3189
  setLoading(false);
3106
3190
  }
3107
- }, [api]);
3191
+ }, [api, workspace]);
3108
3192
  const loadUses = (0, react.useCallback)(async () => {
3109
3193
  try {
3110
3194
  const result = await api.stats();
@@ -3208,13 +3292,13 @@ window.__ModuleLoader__.load({
3208
3292
  setDetailLoading(true);
3209
3293
  setLoadError(null);
3210
3294
  try {
3211
- setDetail(await api.skill(name));
3295
+ setDetail(await api.skill(name, workspace !== "" ? { cwd: workspace } : void 0));
3212
3296
  } catch (error) {
3213
3297
  setLoadError(errorMessage(error));
3214
3298
  } finally {
3215
3299
  setDetailLoading(false);
3216
3300
  }
3217
- }, [api]);
3301
+ }, [api, workspace]);
3218
3302
  const toggle = (0, react.useCallback)(async (skill, enabled) => {
3219
3303
  setBusyNames((previous) => new Set(previous).add(skill.name));
3220
3304
  setLoadError(null);
@@ -3802,7 +3886,7 @@ window.__ModuleLoader__.load({
3802
3886
  const sourceOptions = (0, react.useMemo)(() => {
3803
3887
  const skills = catalog?.skills ?? [];
3804
3888
  const repos = [...new Set(skills.map((skill) => origins[skill.name]).filter((repo) => repo !== void 0))].sort();
3805
- const hasPrivate = skills.some((skill) => origins[skill.name] === void 0);
3889
+ const hasPrivate = skills.some((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3806
3890
  return [...repos, ...hasPrivate ? [PRIVATE_SOURCE] : []];
3807
3891
  }, [catalog, origins]);
3808
3892
  const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => normalized.length === 0 || skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized)), [catalog, normalized]);
@@ -3818,6 +3902,7 @@ window.__ModuleLoader__.load({
3818
3902
  repoImporting,
3819
3903
  repoResult,
3820
3904
  search,
3905
+ workspace,
3821
3906
  detail,
3822
3907
  detailLoading,
3823
3908
  busyNames,
@@ -3859,6 +3944,7 @@ window.__ModuleLoader__.load({
3859
3944
  tagBusy,
3860
3945
  editSearch,
3861
3946
  collapsedGroups,
3947
+ subdividedProjects,
3862
3948
  showLegend,
3863
3949
  actionNames,
3864
3950
  viewNames,
@@ -3874,6 +3960,7 @@ window.__ModuleLoader__.load({
3874
3960
  setLoadError,
3875
3961
  setSuccessBanner,
3876
3962
  setSearch,
3963
+ setWorkspace,
3877
3964
  setDetail,
3878
3965
  setShowForm,
3879
3966
  setFormName,
@@ -3900,6 +3987,7 @@ window.__ModuleLoader__.load({
3900
3987
  setEditSearch,
3901
3988
  setShowLegend,
3902
3989
  toggleGroupCollapse,
3990
+ toggleSubdivide,
3903
3991
  checkUpdate,
3904
3992
  loadMarket,
3905
3993
  openDetail,
@@ -3935,9 +4023,24 @@ window.__ModuleLoader__.load({
3935
4023
  }
3936
4024
  //#endregion
3937
4025
  //#region src/client/panel/SkillHubPanel.tsx
4026
+ /**
4027
+ * The skill hub panel: catalog grouped by tags + source collections, search
4028
+ * and filter in one row, per-group tri-state switches with conflict dialogs,
4029
+ * upstream source tracking (check / sync / follow upstream deletion into a
4030
+ * restorable trash), market sources, disabled re-enable,
4031
+ * detail inspection, and the new-skill scaffold form.
4032
+ *
4033
+ * Thin shell: state and flows live in useSkillHub, the tab contents live in
4034
+ * SourcesView / ScenesView / MarketView, and the dialog family lives in
4035
+ * dialogs.tsx. This component owns only the shared chrome (header, banners,
4036
+ * filter bar, shared sections) and the view routing — and holds no hooks,
4037
+ * so the detail / tag-editor early returns are safe.
4038
+ */
3938
4039
  function SkillHubPanel(props) {
3939
4040
  const hub = useSkillHub(props.api);
3940
- const { catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc, formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search, sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered, conflictDialog, confirmDialog, deleteSkillDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog, syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource, showLegend, setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab, setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch, setShowLegend, checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled } = hub;
4041
+ /** 工作区输入草稿:回车才应用,避免每次按键都触发目录重拉。 */
4042
+ const [workspaceDraft, setWorkspaceDraft] = (0, react.useState)("");
4043
+ const { catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc, formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search, workspace, setWorkspace, sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered, conflictDialog, confirmDialog, deleteSkillDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog, syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource, showLegend, setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab, setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch, setShowLegend, checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled } = hub;
3941
4044
  if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
3942
4045
  detail,
3943
4046
  hubConfig,
@@ -4081,6 +4184,30 @@ window.__ModuleLoader__.load({
4081
4184
  })
4082
4185
  ]
4083
4186
  }),
4187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4188
+ className: panel_module_css_default.workspaceBox,
4189
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4190
+ className: panel_module_css_default.search + " " + panel_module_css_default.workspaceInput,
4191
+ value: workspaceDraft,
4192
+ placeholder: workspace !== "" ? workspace : tt("panel.workspacePlaceholder"),
4193
+ title: tt("panel.workspaceHint"),
4194
+ onChange: (event) => {
4195
+ setWorkspaceDraft(event.target.value);
4196
+ },
4197
+ onKeyDown: (event) => {
4198
+ if (event.key === "Enter") setWorkspace(event.target.value.trim());
4199
+ }
4200
+ }), workspace !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4201
+ type: "button",
4202
+ className: panel_module_css_default.opBtn,
4203
+ title: tt("panel.workspaceClear"),
4204
+ onClick: () => {
4205
+ setWorkspace("");
4206
+ setWorkspaceDraft("");
4207
+ },
4208
+ children: "✕"
4209
+ }) : null]
4210
+ }),
4084
4211
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4085
4212
  type: "button",
4086
4213
  className: panel_module_css_default.legendToggle + (showLegend ? " " + panel_module_css_default.legendToggleActive : ""),
@@ -4473,8 +4600,20 @@ window.__ModuleLoader__.load({
4473
4600
  //#region src/client/index.tsx
4474
4601
  /** Locale namespace this plugin owns. */
4475
4602
  const NS = "dsh-skill-hub";
4476
- /** Required services (fiber inject waiting — the runtime must be up first). */
4477
- const inject = ["slots", "locale"];
4603
+ /**
4604
+ * Required services (fiber inject waiting the runtime must be up first).
4605
+ * `connection`/`remote` are the settings transport's own prerequisites
4606
+ * (`ctx.settingsScope.bind` resolves them on the caller's fiber), and
4607
+ * `settingsScope` is the namespace-scope binder itself; mirror the official
4608
+ * settings-plugins inject list.
4609
+ */
4610
+ const inject = [
4611
+ "slots",
4612
+ "locale",
4613
+ "connection",
4614
+ "remote",
4615
+ "settingsScope"
4616
+ ];
4478
4617
  /**
4479
4618
  * Mount the settings card and the skill hub section.
4480
4619
  * @param ctx - client root context (slots, locale).
@@ -4486,11 +4625,10 @@ window.__ModuleLoader__.load({
4486
4625
  }), "dsh-skill-hub: dictionaries");
4487
4626
  const t = ctx.locale.bind(NS);
4488
4627
  const api = new SkillHubApi();
4489
- const settingsCard = new SkillHubSettingsCardController(new ApiConfigScope(api));
4628
+ const settingsCard = new SkillHubSettingsCardController(ctx.settingsScope.bind({ namespace: NS }));
4490
4629
  ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
4491
4630
  name: "settings.plugin.item",
4492
- id: "dsh-skill-hub",
4493
- order: 115,
4631
+ key: NS,
4494
4632
  locale: NS,
4495
4633
  inject: () => settingsCard.inject()
4496
4634
  }, SkillHubSettingsCard)), "dsh-skill-hub: settings card");