dsh-skill-hub 0.2.0 → 0.2.1

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 (44) 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 +381 -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 +7 -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 +14 -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/SourcesView.tsx +92 -5
  33. package/src/client/panel/panel.module.css +6 -1
  34. package/src/client/panel/useSkillHub.ts +25 -11
  35. package/src/client/settings-card.tsx +5 -4
  36. package/src/index.ts +78 -31
  37. package/src/protocol.ts +24 -0
  38. package/src/routes.test.ts +55 -0
  39. package/src/routes.ts +90 -10
  40. package/src/skillfs.ts +1 -1
  41. package/src/update.ts +1 -1
  42. package/lib/types/client/api-config-scope.d.ts +0 -48
  43. package/src/client/api-config-scope.test.ts +0 -79
  44. 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": "保留开启",
@@ -623,19 +533,19 @@ window.__ModuleLoader__.load({
623
533
  "panel.emptyAll": "No skills installed yet. Create the first one.",
624
534
  "panel.loading": "Loading…",
625
535
  "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.",
536
+ "panel.workspacePlaceholder": "Workspace path (Enter to apply)",
537
+ "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.",
538
+ "panel.workspaceClear": "Clear workspace",
627
539
  "market.addPlaceholder": "owner/repo or https://github.com/…",
628
540
  "market.addSource": "Add source",
629
541
  "market.noSources": "No market sources yet. Add one from the built-in catalog below, or enter a GitHub repo.",
630
- "market.catalogTitle": "Market catalog",
542
+ "market.title": "Market",
631
543
  "market.catalogHint": "Curated skill repos — add one to scan and install, with ongoing update checks.",
632
544
  "market.catalog.anthropics": "Official Anthropic skill collection (Claude Skills)",
633
545
  "market.catalog.superpowers": "Superpowers community skill collection",
634
546
  "market.catalog.mattpocock": "Matt Pocock skill collection",
635
547
  "market.catalog.openDesign": "Design skill collection (design-templates)",
636
- "market.added": "Added",
637
548
  "market.add": "Add",
638
- "market.mySources": "My sources",
639
549
  "market.installed": "{count} installed",
640
550
  "market.updatable": "{count} updatable",
641
551
  "market.deletedUpstream": "{count} deleted upstream",
@@ -700,6 +610,9 @@ window.__ModuleLoader__.load({
700
610
  "groups.empty": "No scenes yet. Create one and start categorizing skills.",
701
611
  "groups.noCollections": "No source groups yet. Import skills from the market to group them by source.",
702
612
  "groups.personal": "Personal",
613
+ "groups.project": "Project",
614
+ "groups.subdivide": "Split",
615
+ "groups.merge": "Merge",
703
616
  "groups.noWritable": "No writable skills in this group (read-only skills cannot be switched from the hub)",
704
617
  "groups.closeAll": "Close all",
705
618
  "groups.keepOn": "Keep on",
@@ -1301,11 +1214,17 @@ window.__ModuleLoader__.load({
1301
1214
  };
1302
1215
  //#endregion
1303
1216
  //#region src/client/SkillHubSettingsCard.tsx
1217
+ /** Model-invocable dot color default. Single source for the TS side; the
1218
+ * panel's CSS mirrors it via --hub-model (panel.module.css). */
1219
+ const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
1220
+ /** User-invocable dot color default. Single source for the TS side; the
1221
+ * panel's CSS mirrors it via --hub-user (panel.module.css). */
1222
+ const DEFAULT_DOT_USER_COLOR = "#3fb950";
1304
1223
  /** Bridges the hub's config scope onto the card's staged form. */
1305
1224
  var SkillHubSettingsCardController = class {
1306
1225
  form;
1307
1226
  store;
1308
- /** @param scope - the hub config scope the card edits (ApiConfigScope). */
1227
+ /** @param scope - the hub settings scope the card edits (FormScope-compatible). */
1309
1228
  constructor(scope) {
1310
1229
  this.form = new CardForm(scope, [
1311
1230
  booleanField("enabled"),
@@ -1512,15 +1431,23 @@ window.__ModuleLoader__.load({
1512
1431
  }
1513
1432
  /** Origin-repo filter value: skills with no source record (private skills). */
1514
1433
  const PRIVATE_SOURCE = "private";
1434
+ /** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
1435
+ function isProjectSource(source) {
1436
+ return source === "project-dsh" || source === "project-agents";
1437
+ }
1515
1438
  /**
1516
1439
  * Apply the origin filter ('all' or a specific origin repo; skills without a
1517
1440
  * source record count as PRIVATE_SOURCE). The origins map is the store's
1518
1441
  * skillName → repo derivation, so filtering follows the tracked source
1519
1442
  * records instead of the filesystem root a skill happens to live under.
1443
+ * 项目级技能(有 workspace 归属)永远不算「个人」。
1520
1444
  */
1521
1445
  function filterBySource(skills, source, origins) {
1522
1446
  if (source === "all") return [...skills];
1523
- return skills.filter((skill) => (origins[skill.name] ?? "private") === source);
1447
+ return skills.filter((skill) => {
1448
+ if (isProjectSource(skill.source)) return false;
1449
+ return (origins[skill.name] ?? "private") === source;
1450
+ });
1524
1451
  }
1525
1452
  /**
1526
1453
  * Sort a skill list in place-safe copy order: name ascending, added
@@ -1583,7 +1510,7 @@ window.__ModuleLoader__.load({
1583
1510
  }
1584
1511
  //#endregion
1585
1512
  //#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}";
1513
+ 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}._6VtqdG_workspaceBox{align-items:center;gap:6px;display:inline-flex}._6VtqdG_workspaceInput{width:200px;padding:6px 10px;font-size:12px}._6VtqdG_projectNest{margin-left:14px}";
1587
1514
  const tagId = "dsh-skill-hub/panel.module.css";
1588
1515
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1589
1516
  const tag = document.createElement("style");
@@ -1664,6 +1591,7 @@ window.__ModuleLoader__.load({
1664
1591
  "opDanger": "_6VtqdG_opDanger",
1665
1592
  "panel": "_6VtqdG_panel",
1666
1593
  "primary": "_6VtqdG_primary",
1594
+ "projectNest": "_6VtqdG_projectNest",
1667
1595
  "row": "_6VtqdG_row",
1668
1596
  "rowDesc": "_6VtqdG_rowDesc",
1669
1597
  "rowMain": "_6VtqdG_rowMain",
@@ -1701,7 +1629,9 @@ window.__ModuleLoader__.load({
1701
1629
  "titleIcon": "_6VtqdG_titleIcon",
1702
1630
  "updateLink": "_6VtqdG_updateLink",
1703
1631
  "useCount": "_6VtqdG_useCount",
1704
- "useTime": "_6VtqdG_useTime"
1632
+ "useTime": "_6VtqdG_useTime",
1633
+ "workspaceBox": "_6VtqdG_workspaceBox",
1634
+ "workspaceInput": "_6VtqdG_workspaceInput"
1705
1635
  };
1706
1636
  //#endregion
1707
1637
  //#region src/client/panel/SourceStatusBadge.tsx
@@ -2187,6 +2117,123 @@ window.__ModuleLoader__.load({
2187
2117
  }
2188
2118
  //#endregion
2189
2119
  //#region src/client/panel/SourcesView.tsx
2120
+ /** 项目级三级树:项目级 → 具体工作区(可折叠、可细分)→ .dsh/.agents。 */
2121
+ function ProjectTree(props) {
2122
+ const { hub } = props;
2123
+ const { sorted, sourceFilter, origins, collapsedGroups, subdividedProjects, toggleGroupCollapse, toggleSubdivide } = hub;
2124
+ const projectSkills = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
2125
+ if (projectSkills.length === 0) return null;
2126
+ const byProject = /* @__PURE__ */ new Map();
2127
+ for (const skill of projectSkills) {
2128
+ const key = skill.workspace ?? skill.source;
2129
+ const entry = byProject.get(key);
2130
+ if (entry === void 0) byProject.set(key, {
2131
+ title: skill.workspaceTitle ?? skill.workspace ?? tt("groups.project"),
2132
+ skills: [skill]
2133
+ });
2134
+ else entry.skills.push(skill);
2135
+ }
2136
+ const topCollapsed = collapsedGroups.has("project");
2137
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2138
+ className: panel_module_css_default.section,
2139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2140
+ className: panel_module_css_default.groupHead,
2141
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2142
+ type: "button",
2143
+ className: panel_module_css_default.disclosure,
2144
+ "aria-expanded": !topCollapsed,
2145
+ onClick: () => {
2146
+ toggleGroupCollapse("project");
2147
+ },
2148
+ 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", {
2149
+ className: panel_module_css_default.groupTitle,
2150
+ children: [
2151
+ tt("groups.project"),
2152
+ " · ",
2153
+ byProject.size
2154
+ ]
2155
+ })]
2156
+ })
2157
+ }), !topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
2158
+ const projKey = "project:" + key;
2159
+ const projCollapsed = collapsedGroups.has(projKey);
2160
+ const subdivided = subdividedProjects.has(key);
2161
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2162
+ className: panel_module_css_default.section + " " + panel_module_css_default.projectNest,
2163
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2164
+ className: panel_module_css_default.groupHead,
2165
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2166
+ type: "button",
2167
+ className: panel_module_css_default.disclosure,
2168
+ "aria-expanded": !projCollapsed,
2169
+ onClick: () => {
2170
+ toggleGroupCollapse(projKey);
2171
+ },
2172
+ 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", {
2173
+ className: panel_module_css_default.groupTitle,
2174
+ children: [
2175
+ proj.title,
2176
+ " · ",
2177
+ proj.skills.length,
2178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
2179
+ members: proj.skills.map((skill) => skill.name),
2180
+ hub
2181
+ })
2182
+ ]
2183
+ })]
2184
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2185
+ className: panel_module_css_default.groupOps,
2186
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2187
+ type: "button",
2188
+ className: panel_module_css_default.opBtn,
2189
+ onClick: (event) => {
2190
+ event.stopPropagation();
2191
+ toggleSubdivide(key);
2192
+ },
2193
+ children: subdivided ? tt("groups.merge") : tt("groups.subdivide")
2194
+ })
2195
+ })]
2196
+ }), !projCollapsed ? subdivided ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2197
+ className: panel_module_css_default.projectNest,
2198
+ children: ["project-dsh", "project-agents"].map((source) => {
2199
+ const list = proj.skills.filter((skill) => skill.source === source);
2200
+ if (list.length === 0) return null;
2201
+ const srcKey = projKey + ":" + source;
2202
+ const srcCollapsed = collapsedGroups.has(srcKey);
2203
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
+ className: panel_module_css_default.section + " " + panel_module_css_default.projectNest,
2205
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2206
+ className: panel_module_css_default.groupHead,
2207
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2208
+ type: "button",
2209
+ className: panel_module_css_default.disclosure,
2210
+ "aria-expanded": !srcCollapsed,
2211
+ onClick: () => {
2212
+ toggleGroupCollapse(srcKey);
2213
+ },
2214
+ 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", {
2215
+ className: panel_module_css_default.groupTitle,
2216
+ children: [
2217
+ tt("badge.source." + source),
2218
+ " · ",
2219
+ list.length
2220
+ ]
2221
+ })]
2222
+ })
2223
+ }), !srcCollapsed ? list.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2224
+ skill,
2225
+ hub
2226
+ }, skill.name)) : null]
2227
+ }, srcKey);
2228
+ })
2229
+ }) : proj.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
2230
+ skill,
2231
+ hub
2232
+ }, skill.name)) : null]
2233
+ }, projKey);
2234
+ }) : null]
2235
+ });
2236
+ }
2190
2237
  function SourcesView(props) {
2191
2238
  const { hub } = props;
2192
2239
  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 +2242,7 @@ window.__ModuleLoader__.load({
2195
2242
  hub
2196
2243
  }, skill.name)) });
2197
2244
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTree, { hub }),
2198
2246
  groupsState !== null && groupsState.collections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2199
2247
  className: panel_module_css_default.empty,
2200
2248
  children: tt("groups.noCollections")
@@ -2295,7 +2343,7 @@ window.__ModuleLoader__.load({
2295
2343
  }, "col:" + collection.name);
2296
2344
  }),
2297
2345
  (() => {
2298
- const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0);
2346
+ const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
2299
2347
  if (uncategorized.length === 0) return null;
2300
2348
  const collapsed = collapsedGroups.has("uncategorized-source");
2301
2349
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -2658,6 +2706,12 @@ window.__ModuleLoader__.load({
2658
2706
  }
2659
2707
  //#endregion
2660
2708
  //#region src/client/panel/MarketView.tsx
2709
+ /**
2710
+ * Market tab: one unified market list — built-in catalog entries (add
2711
+ * button while not yet added) and the user's market sources (full state
2712
+ * badges + actions once added, plus custom sources), with check-all and
2713
+ * update-all actions and the repo scan result with the importable checklist.
2714
+ */
2661
2715
  /** Compact byte size for repo preview rows. */
2662
2716
  function formatBytes(bytes) {
2663
2717
  if (bytes < 1024) return bytes + " B";
@@ -2669,54 +2723,140 @@ window.__ModuleLoader__.load({
2669
2723
  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
2724
  /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
2671
2725
  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,
2726
+ /** 一个已添加来源的完整行:状态徽章 + 操作按钮。 */
2727
+ const sourceRow = (record) => {
2728
+ const releaseCheck = marketCheck[record.repo];
2729
+ const skillCheck = sourceCheck[record.repo];
2730
+ const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
2731
+ const scanning = scanningRepo === record.repo;
2732
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2733
+ className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2675
2734
  children: [
2676
2735
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2677
- className: panel_module_css_default.sectionTitle,
2678
- children: tt("market.catalogTitle")
2736
+ className: panel_module_css_default.rowMain,
2737
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2738
+ className: panel_module_css_default.rowName,
2739
+ children: [
2740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2741
+ className: panel_module_css_default.sourceLink,
2742
+ href: "https://github.com/" + record.repo,
2743
+ target: "_blank",
2744
+ rel: "noreferrer",
2745
+ children: record.repo
2746
+ }),
2747
+ record.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2748
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
2749
+ children: record.ref
2750
+ }) : null,
2751
+ installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2752
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
2753
+ children: tt("market.installed", { count: installedCount })
2754
+ }) : null,
2755
+ skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2756
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2757
+ children: tt("market.updatable", { count: skillCheck.updated.length })
2758
+ }) : null,
2759
+ skillCheck !== void 0 && skillCheck.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2760
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
2761
+ children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
2762
+ }) : null,
2763
+ releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2764
+ className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
2765
+ children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
2766
+ }) : null
2767
+ ]
2768
+ })
2679
2769
  }),
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")
2770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2771
+ type: "button",
2772
+ className: panel_module_css_default.opBtn,
2773
+ disabled: scanning,
2774
+ onClick: () => {
2775
+ checkMarket();
2776
+ },
2777
+ children: tt("market.check")
2683
2778
  }),
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);
2779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2780
+ type: "button",
2781
+ className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
2782
+ disabled: syncingMarket === record.repo,
2783
+ onClick: () => {
2784
+ syncMarketSource(record.repo);
2785
+ },
2786
+ children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
2787
+ }),
2788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2789
+ type: "button",
2790
+ className: panel_module_css_default.opBtn,
2791
+ disabled: scanning,
2792
+ onClick: () => {
2793
+ scanRepo(record.repo);
2794
+ },
2795
+ children: scanning ? tt("market.scanning") : tt("market.scan")
2796
+ }),
2797
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2798
+ type: "button",
2799
+ className: panel_module_css_default.opBtn,
2800
+ disabled: tagBusy,
2801
+ title: tt("market.removeHint"),
2802
+ onClick: () => {
2803
+ removeMarketSource(record.repo);
2804
+ },
2805
+ children: tt("market.deleteSource")
2717
2806
  })
2718
2807
  ]
2719
- }),
2808
+ });
2809
+ };
2810
+ const rows = [];
2811
+ for (const entry of MARKET_CATALOG) {
2812
+ const record = marketState.repos.find((item) => item.repo === entry.repo);
2813
+ if (record !== void 0) rows.push({
2814
+ key: entry.repo,
2815
+ element: sourceRow(record)
2816
+ });
2817
+ else {
2818
+ const busy = scanningRepo !== null || tagBusy;
2819
+ rows.push({
2820
+ key: entry.repo,
2821
+ element: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2822
+ className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
2823
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2824
+ className: panel_module_css_default.rowMain,
2825
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2826
+ className: panel_module_css_default.rowName,
2827
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2828
+ className: panel_module_css_default.sourceLink,
2829
+ href: "https://github.com/" + entry.repo,
2830
+ target: "_blank",
2831
+ rel: "noreferrer",
2832
+ children: entry.repo
2833
+ })
2834
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2835
+ className: panel_module_css_default.rowDesc,
2836
+ children: tt(entry.descriptionKey)
2837
+ })]
2838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2839
+ type: "button",
2840
+ className: panel_module_css_default.opBtn,
2841
+ disabled: busy,
2842
+ onClick: () => {
2843
+ addSource(entry.repo);
2844
+ },
2845
+ children: tt("market.add")
2846
+ })]
2847
+ })
2848
+ });
2849
+ }
2850
+ }
2851
+ for (const record of marketState.repos) {
2852
+ if (MARKET_CATALOG.some((entry) => entry.repo === record.repo)) continue;
2853
+ rows.push({
2854
+ key: record.repo,
2855
+ element: sourceRow(record)
2856
+ });
2857
+ }
2858
+ const unaddedCatalog = MARKET_CATALOG.filter((entry) => !marketState.repos.some((record) => record.repo === entry.repo));
2859
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2720
2860
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2721
2861
  className: panel_module_css_default.section,
2722
2862
  children: [
@@ -2725,7 +2865,7 @@ window.__ModuleLoader__.load({
2725
2865
  children: [
2726
2866
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2727
2867
  className: panel_module_css_default.sectionTitleFill,
2728
- children: tt("market.mySources")
2868
+ children: tt("market.title")
2729
2869
  }),
2730
2870
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2731
2871
  type: "button",
@@ -2747,10 +2887,6 @@ window.__ModuleLoader__.load({
2747
2887
  })
2748
2888
  ]
2749
2889
  }),
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
2890
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2755
2891
  className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsPadded,
2756
2892
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -2776,97 +2912,19 @@ window.__ModuleLoader__.load({
2776
2912
  children: tt("market.addSource")
2777
2913
  })]
2778
2914
  }),
2915
+ unaddedCatalog.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2916
+ className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
2917
+ children: tt("market.catalogHint")
2918
+ }) : null,
2779
2919
  marketState.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2780
2920
  className: panel_module_css_default.empty,
2781
2921
  children: tt("panel.loading")
2782
2922
  }) : null,
2783
- marketState.status === "ready" && marketState.repos.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2923
+ marketState.status !== "loading" && rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2784
2924
  className: panel_module_css_default.empty,
2785
2925
  children: tt("market.noSources")
2786
2926
  }) : 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
- })
2927
+ marketState.status !== "loading" ? rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: row.element }, row.key)) : null
2870
2928
  ]
2871
2929
  }),
2872
2930
  repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3039,6 +3097,8 @@ window.__ModuleLoader__.load({
3039
3097
  const [repoImporting, setRepoImporting] = (0, react.useState)(false);
3040
3098
  const [repoResult, setRepoResult] = (0, react.useState)(null);
3041
3099
  const [search, setSearch] = (0, react.useState)("");
3100
+ /** 工作区(项目)路径;空 = 只看用户级技能。 */
3101
+ const [workspace, setWorkspace] = (0, react.useState)("");
3042
3102
  const [detail, setDetail] = (0, react.useState)(null);
3043
3103
  const [detailLoading, setDetailLoading] = (0, react.useState)(false);
3044
3104
  const [busyNames, setBusyNames] = (0, react.useState)(/* @__PURE__ */ new Set());
@@ -3083,8 +3143,10 @@ window.__ModuleLoader__.load({
3083
3143
  const [newTagName, setNewTagName] = (0, react.useState)("");
3084
3144
  const [tagBusy, setTagBusy] = (0, react.useState)(false);
3085
3145
  const [editSearch, setEditSearch] = (0, react.useState)("");
3086
- /** 分组视图里收起的分组(key 为 tag:<id> 或 col:<name>)。 */
3146
+ /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。 */
3087
3147
  const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
3148
+ /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
3149
+ const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
3088
3150
  const [showLegend, setShowLegend] = (0, react.useState)(false);
3089
3151
  const toggleGroupCollapse = (0, react.useCallback)((key) => {
3090
3152
  setCollapsedGroups((previous) => {
@@ -3094,9 +3156,17 @@ window.__ModuleLoader__.load({
3094
3156
  return next;
3095
3157
  });
3096
3158
  }, []);
3159
+ const toggleSubdivide = (0, react.useCallback)((key) => {
3160
+ setSubdividedProjects((previous) => {
3161
+ const next = new Set(previous);
3162
+ if (next.has(key)) next.delete(key);
3163
+ else next.add(key);
3164
+ return next;
3165
+ });
3166
+ }, []);
3097
3167
  const load = (0, react.useCallback)(async () => {
3098
3168
  try {
3099
- const next = await api.catalog();
3169
+ const next = await api.catalog(workspace !== "" ? { cwd: workspace } : void 0);
3100
3170
  setCatalog(next);
3101
3171
  setLoadError(null);
3102
3172
  } catch (error) {
@@ -3104,7 +3174,7 @@ window.__ModuleLoader__.load({
3104
3174
  } finally {
3105
3175
  setLoading(false);
3106
3176
  }
3107
- }, [api]);
3177
+ }, [api, workspace]);
3108
3178
  const loadUses = (0, react.useCallback)(async () => {
3109
3179
  try {
3110
3180
  const result = await api.stats();
@@ -3208,13 +3278,13 @@ window.__ModuleLoader__.load({
3208
3278
  setDetailLoading(true);
3209
3279
  setLoadError(null);
3210
3280
  try {
3211
- setDetail(await api.skill(name));
3281
+ setDetail(await api.skill(name, workspace !== "" ? { cwd: workspace } : void 0));
3212
3282
  } catch (error) {
3213
3283
  setLoadError(errorMessage(error));
3214
3284
  } finally {
3215
3285
  setDetailLoading(false);
3216
3286
  }
3217
- }, [api]);
3287
+ }, [api, workspace]);
3218
3288
  const toggle = (0, react.useCallback)(async (skill, enabled) => {
3219
3289
  setBusyNames((previous) => new Set(previous).add(skill.name));
3220
3290
  setLoadError(null);
@@ -3802,7 +3872,7 @@ window.__ModuleLoader__.load({
3802
3872
  const sourceOptions = (0, react.useMemo)(() => {
3803
3873
  const skills = catalog?.skills ?? [];
3804
3874
  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);
3875
+ const hasPrivate = skills.some((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3806
3876
  return [...repos, ...hasPrivate ? [PRIVATE_SOURCE] : []];
3807
3877
  }, [catalog, origins]);
3808
3878
  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 +3888,7 @@ window.__ModuleLoader__.load({
3818
3888
  repoImporting,
3819
3889
  repoResult,
3820
3890
  search,
3891
+ workspace,
3821
3892
  detail,
3822
3893
  detailLoading,
3823
3894
  busyNames,
@@ -3859,6 +3930,7 @@ window.__ModuleLoader__.load({
3859
3930
  tagBusy,
3860
3931
  editSearch,
3861
3932
  collapsedGroups,
3933
+ subdividedProjects,
3862
3934
  showLegend,
3863
3935
  actionNames,
3864
3936
  viewNames,
@@ -3874,6 +3946,7 @@ window.__ModuleLoader__.load({
3874
3946
  setLoadError,
3875
3947
  setSuccessBanner,
3876
3948
  setSearch,
3949
+ setWorkspace,
3877
3950
  setDetail,
3878
3951
  setShowForm,
3879
3952
  setFormName,
@@ -3900,6 +3973,7 @@ window.__ModuleLoader__.load({
3900
3973
  setEditSearch,
3901
3974
  setShowLegend,
3902
3975
  toggleGroupCollapse,
3976
+ toggleSubdivide,
3903
3977
  checkUpdate,
3904
3978
  loadMarket,
3905
3979
  openDetail,
@@ -3935,9 +4009,24 @@ window.__ModuleLoader__.load({
3935
4009
  }
3936
4010
  //#endregion
3937
4011
  //#region src/client/panel/SkillHubPanel.tsx
4012
+ /**
4013
+ * The skill hub panel: catalog grouped by tags + source collections, search
4014
+ * and filter in one row, per-group tri-state switches with conflict dialogs,
4015
+ * upstream source tracking (check / sync / follow upstream deletion into a
4016
+ * restorable trash), market sources, disabled re-enable,
4017
+ * detail inspection, and the new-skill scaffold form.
4018
+ *
4019
+ * Thin shell: state and flows live in useSkillHub, the tab contents live in
4020
+ * SourcesView / ScenesView / MarketView, and the dialog family lives in
4021
+ * dialogs.tsx. This component owns only the shared chrome (header, banners,
4022
+ * filter bar, shared sections) and the view routing — and holds no hooks,
4023
+ * so the detail / tag-editor early returns are safe.
4024
+ */
3938
4025
  function SkillHubPanel(props) {
3939
4026
  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;
4027
+ /** 工作区输入草稿:回车才应用,避免每次按键都触发目录重拉。 */
4028
+ const [workspaceDraft, setWorkspaceDraft] = (0, react.useState)("");
4029
+ 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
4030
  if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
3942
4031
  detail,
3943
4032
  hubConfig,
@@ -4081,6 +4170,30 @@ window.__ModuleLoader__.load({
4081
4170
  })
4082
4171
  ]
4083
4172
  }),
4173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4174
+ className: panel_module_css_default.workspaceBox,
4175
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4176
+ className: panel_module_css_default.search + " " + panel_module_css_default.workspaceInput,
4177
+ value: workspaceDraft,
4178
+ placeholder: workspace !== "" ? workspace : tt("panel.workspacePlaceholder"),
4179
+ title: tt("panel.workspaceHint"),
4180
+ onChange: (event) => {
4181
+ setWorkspaceDraft(event.target.value);
4182
+ },
4183
+ onKeyDown: (event) => {
4184
+ if (event.key === "Enter") setWorkspace(event.target.value.trim());
4185
+ }
4186
+ }), workspace !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4187
+ type: "button",
4188
+ className: panel_module_css_default.opBtn,
4189
+ title: tt("panel.workspaceClear"),
4190
+ onClick: () => {
4191
+ setWorkspace("");
4192
+ setWorkspaceDraft("");
4193
+ },
4194
+ children: "✕"
4195
+ }) : null]
4196
+ }),
4084
4197
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4085
4198
  type: "button",
4086
4199
  className: panel_module_css_default.legendToggle + (showLegend ? " " + panel_module_css_default.legendToggleActive : ""),
@@ -4473,8 +4586,20 @@ window.__ModuleLoader__.load({
4473
4586
  //#region src/client/index.tsx
4474
4587
  /** Locale namespace this plugin owns. */
4475
4588
  const NS = "dsh-skill-hub";
4476
- /** Required services (fiber inject waiting — the runtime must be up first). */
4477
- const inject = ["slots", "locale"];
4589
+ /**
4590
+ * Required services (fiber inject waiting — the runtime must be up first).
4591
+ * `connection`/`remote` are the settings transport's own prerequisites
4592
+ * (`ctx.settingsScope.bind` resolves them on the caller's fiber), and
4593
+ * `settingsScope` is the namespace-scope binder itself; mirror the official
4594
+ * settings-plugins inject list.
4595
+ */
4596
+ const inject = [
4597
+ "slots",
4598
+ "locale",
4599
+ "connection",
4600
+ "remote",
4601
+ "settingsScope"
4602
+ ];
4478
4603
  /**
4479
4604
  * Mount the settings card and the skill hub section.
4480
4605
  * @param ctx - client root context (slots, locale).
@@ -4486,11 +4611,10 @@ window.__ModuleLoader__.load({
4486
4611
  }), "dsh-skill-hub: dictionaries");
4487
4612
  const t = ctx.locale.bind(NS);
4488
4613
  const api = new SkillHubApi();
4489
- const settingsCard = new SkillHubSettingsCardController(new ApiConfigScope(api));
4614
+ const settingsCard = new SkillHubSettingsCardController(ctx.settingsScope.bind({ namespace: NS }));
4490
4615
  ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
4491
4616
  name: "settings.plugin.item",
4492
- id: "dsh-skill-hub",
4493
- order: 115,
4617
+ key: NS,
4494
4618
  locale: NS,
4495
4619
  inject: () => settingsCard.inject()
4496
4620
  }, SkillHubSettingsCard)), "dsh-skill-hub: settings card");