@morehao/dsh-skills-console 0.5.0 → 0.5.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.
package/client.js CHANGED
@@ -46,12 +46,13 @@ var API = "/api/morehao/skills-console";
46
46
  function short(sha) {
47
47
  return sha ? String(sha).slice(0, 7) : "";
48
48
  }
49
- async function apiCall(method, path, body) {
49
+ async function apiCall(method, path, body, options = {}) {
50
50
  const response = await fetch(`${API}${path}`, {
51
51
  method,
52
52
  headers: { "content-type": "application/json", "x-dsh-skills-console": "1" },
53
53
  body: body === void 0 ? void 0 : JSON.stringify(body),
54
- cache: "no-store"
54
+ cache: "no-store",
55
+ ...options.signal ? { signal: options.signal } : {}
55
56
  });
56
57
  let json = {};
57
58
  try {
@@ -302,7 +303,33 @@ var DICT = {
302
303
  "viewer.info.visibility": "可见性",
303
304
  "viewer.info.mode": "状态",
304
305
  "viewer.info.status": "更新",
305
- "viewer.info.none": "无"
306
+ "viewer.info.none": "无",
307
+ "intro.button": "介绍",
308
+ "intro.open": "AI 介绍:这个技能能做什么",
309
+ "intro.title": "AI 介绍",
310
+ "intro.loading": "读取中…",
311
+ "intro.empty": "还没有生成介绍",
312
+ "intro.generate": "生成介绍",
313
+ "intro.regenerate": "重新生成",
314
+ "intro.generating": "生成中…",
315
+ "intro.hint": "会把 SKILL.md 正文发送给当前配置的模型提供方",
316
+ "intro.hintRemote": "会把该技能在 {source}@{commit} 的 SKILL.md 发送给当前配置的模型提供方",
317
+ "intro.meta": "{model} · {time} · AI 生成,可能不准确",
318
+ "intro.stale": "技能内容已更新,介绍可能过期",
319
+ "intro.view": "查看技能",
320
+ "intro.unavailable": "当前宿主没有可用的模型服务",
321
+ "intro.error.noModel": "还没有配置可用的模型",
322
+ "intro.error.rateLimit": "模型限流或额度不足,请稍后再试",
323
+ "intro.error.tooLarge": "技能正文超出模型上下文",
324
+ "intro.error.timeout": "生成超时,请重试",
325
+ "intro.error.auth": "模型提供方认证失败",
326
+ "intro.error.empty": "模型没有返回内容",
327
+ "intro.error.busy": "正在生成,请稍候",
328
+ "intro.error.source": "读不到远端的 SKILL.md(仓库私有、已删除或网络问题)",
329
+ "intro.error.noSource": "订阅快照里没有这个技能,请先点「检查更新」",
330
+ "intro.error.subscription": "订阅不存在,可能已被取消",
331
+ "intro.error.sourceTimeout": "读取远端 SKILL.md 超时,请重试",
332
+ "intro.error.generic": "生成失败,请重试"
306
333
  },
307
334
  en: {
308
335
  "section.title": "Skills Console",
@@ -535,7 +562,33 @@ var DICT = {
535
562
  "viewer.info.visibility": "Visibility",
536
563
  "viewer.info.mode": "State",
537
564
  "viewer.info.status": "Updates",
538
- "viewer.info.none": "None"
565
+ "viewer.info.none": "None",
566
+ "intro.button": "Intro",
567
+ "intro.open": "AI intro: what this skill does",
568
+ "intro.title": "AI intro",
569
+ "intro.loading": "Loading…",
570
+ "intro.empty": "No intro yet",
571
+ "intro.generate": "Generate intro",
572
+ "intro.regenerate": "Regenerate",
573
+ "intro.generating": "Generating…",
574
+ "intro.hint": "Sends the SKILL.md body to the model provider configured in DSH",
575
+ "intro.hintRemote": "Sends the SKILL.md at {source}@{commit} to the model provider configured in DSH",
576
+ "intro.meta": "{model} · {time} · AI-generated, may be inaccurate",
577
+ "intro.stale": "The skill changed; this intro may be out of date",
578
+ "intro.view": "Open skill",
579
+ "intro.unavailable": "No model service is available in this host",
580
+ "intro.error.noModel": "No usable model is configured",
581
+ "intro.error.rateLimit": "The model is rate-limited or out of quota; try again later",
582
+ "intro.error.tooLarge": "The skill body exceeds the model context window",
583
+ "intro.error.timeout": "Generation timed out; try again",
584
+ "intro.error.auth": "The model provider rejected the credentials",
585
+ "intro.error.empty": "The model returned no content",
586
+ "intro.error.busy": "Already generating; please wait",
587
+ "intro.error.source": "Could not read the remote SKILL.md (private repo, deleted, or network)",
588
+ "intro.error.noSource": "This skill is not in the subscription snapshot; run “Check for updates” first",
589
+ "intro.error.subscription": "The subscription no longer exists",
590
+ "intro.error.sourceTimeout": "Timed out reading the remote SKILL.md; try again",
591
+ "intro.error.generic": "Generation failed; try again"
539
592
  }
540
593
  };
541
594
  function buildTranslate(locale) {
@@ -546,11 +599,16 @@ function buildTranslate(locale) {
546
599
  return "zh";
547
600
  }
548
601
  };
549
- return (key) => {
602
+ const translate = (key) => {
550
603
  const id = active();
551
604
  const pack = DICT[id] || DICT.zh;
552
605
  return pack[key] ?? DICT.zh[key] ?? key;
553
606
  };
607
+ Object.defineProperty(translate, "lang", {
608
+ get: () => active() === "en" ? "en" : "zh",
609
+ enumerable: false
610
+ });
611
+ return translate;
554
612
  }
555
613
 
556
614
  // client/host.js
@@ -982,7 +1040,7 @@ function FolderIcon({ size = 12 }) {
982
1040
  }
983
1041
 
984
1042
  // client/section.js
985
- var import_react8 = __toESM(require("react"), 1);
1043
+ var import_react10 = __toESM(require("react"), 1);
986
1044
 
987
1045
  // client/scroll.js
988
1046
  function isScroller(node, tolerance = 4) {
@@ -1076,6 +1134,19 @@ var CSS = `
1076
1134
  .dshsc-pop-label em { font-style: normal; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
1077
1135
  .dshsc-pop-name { display: inline-flex; align-items: baseline; gap: 6px; }
1078
1136
  .dshsc-pop-hint { font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
1137
+ /* 与 DSH 自带开关同尺寸/同 token:关是 border-l3 轨道,开是 brand-primary 轨道。
1138
+ 0.5.0 换三档 chip 时这组规则被一起删掉,但「侧边栏入口」还在用 .dshsc-switch,补回来。 */
1139
+ .dshsc-switch { box-sizing: border-box; position: relative; flex: none; width: 36px; height: 20px; padding: 2px;
1140
+ border: 0; border-radius: 999px; background: var(--dsw-alias-border-l3); cursor: pointer;
1141
+ transition: background .14s ease; }
1142
+ .dshsc-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1143
+ .dshsc-switch:disabled { cursor: not-allowed; opacity: .5; }
1144
+ .dshsc-switch[aria-checked="true"] { background: var(--dsw-alias-brand-primary); }
1145
+ .dshsc-knob { display: block; width: 16px; height: 16px; border-radius: 50%;
1146
+ background: var(--dsw-alias-switch-thumb, #fff); box-shadow: 0 1px 2px rgba(0, 0, 0, .28);
1147
+ transition: transform .12s ease, background .12s ease; }
1148
+ .dshsc-switch[aria-checked="true"] .dshsc-knob { transform: translateX(16px);
1149
+ background: var(--dsw-alias-label-primary-foreground, #fff); }
1079
1150
 
1080
1151
  .dshsc-btn { font: inherit; font-size: 12px; line-height: 1; padding: 7px 11px; border-radius: 8px; cursor: pointer;
1081
1152
  white-space: nowrap;
@@ -1299,20 +1370,31 @@ var CSS = `
1299
1370
  .dshsc-sub-list { display: flex; flex-direction: column; padding: 0 12px 7px 46px;
1300
1371
  background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 22%, transparent);
1301
1372
  border-top: 1px solid var(--dsw-alias-border-l1); }
1373
+ /* 条目操作列 92px 是照两枚按钮预留的,眼下每行只有一枚「安装」(两字 ≈ 43px:0 9px padding + 边框)。
1374
+ 表头「操作」和已装行的破折号都按按钮宽度做一个右对齐的居中盒子:三者落在同一条中轴上,
1375
+ 既不贴着列右缘,也不用改列宽 / 挪按钮。换语言或字号时盒子只是不再严格等于按钮宽度。 */
1376
+ .dshsc-sub-list { --dshsc-entry-act: 44px; }
1302
1377
  .dshsc-entryhead { display: grid; grid-template-columns: minmax(0, 1fr) 110px 92px; gap: 10px;
1303
1378
  align-items: center; height: 26px; border-bottom: 1px solid var(--dsw-alias-border-l1);
1304
1379
  color: var(--dsw-alias-label-tertiary); font-size: 10px; font-weight: 500; letter-spacing: .02em; }
1305
- .dshsc-entryhead .right { text-align: right; padding-right: 2px; }
1380
+ .dshsc-entryhead .right { justify-self: end; box-sizing: border-box; width: var(--dshsc-entry-act);
1381
+ text-align: center; padding-right: 0; }
1306
1382
  .dshsc-sub-entry { display: grid; grid-template-columns: minmax(0, 1fr) 110px 92px; gap: 10px;
1307
1383
  align-items: center; font-size: 12px; padding: 6px 0; border-top: 1px solid var(--dsw-alias-border-l1); }
1308
1384
  .dshsc-entryhead + .dshsc-sub-entry { border-top: 0; }
1309
1385
  .dshsc-sub-entry .dshsc-name { font-size: 12px; font-weight: 500; }
1386
+ /* 条目第一列:名字 + ✨ 同一行,作者原描述单独一行(超长省略,完整内容在 title) */
1387
+ .dshsc-sub-skill { min-width: 0; }
1388
+ .dshsc-sub-namerow { display: flex; align-items: center; gap: 6px; min-width: 0; }
1389
+ .dshsc-sub-desc { margin-top: 1px; font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary);
1390
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1310
1391
  .dshsc-state { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
1311
1392
  .dshsc-state.ok svg { color: var(--dsw-alias-state-success-primary); }
1312
1393
  .dshsc-sub-entry .dshsc-actions { justify-self: end; display: flex; justify-content: flex-end; }
1313
1394
  .dshsc-sub-entry .dshsc-btn { height: 24px; padding: 0 9px; font-size: 11.5px; }
1314
- /* 已装条目的操作列没有动作:破折号占位,保持右对齐与按钮基线一致 */
1315
- .dshsc-sub-entry .dshsc-noop { color: var(--dsw-alias-label-tertiary); font-size: 11.5px; line-height: 24px; padding-right: 9px; }
1395
+ /* 已装条目的操作列没有动作:破折号占位,居中在按钮宽度里,和表头、安装按钮同一条中轴 */
1396
+ .dshsc-sub-entry .dshsc-noop { box-sizing: border-box; width: var(--dshsc-entry-act); padding: 0;
1397
+ text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 11.5px; line-height: 24px; }
1316
1398
  /* 设置页的真实宽度常常远小于窗口(侧栏 + 页面缩放),视口 media query 不可靠;
1317
1399
  以 .dshsc-root 自身宽度为准,窄面板时切两列,避免固定列把 Agent 标签挤成竖排。
1318
1400
  断点取 860 而不是贴着面板常见的 ~900:四列网格需要 280 + 150 + 操作列 + 3×14 gap + 中间列,
@@ -1341,7 +1423,7 @@ var CSS = `
1341
1423
  }
1342
1424
  .dshsc-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
1343
1425
 
1344
- /* 调用策略 chip:一个入口显示当前模式;点开三档弹层。窄列里允许省略,不撑破 102px */
1426
+ /* 调用策略 chip:一个入口显示当前模式;点开三档弹窗。窄列里允许省略,不撑破 102px */
1345
1427
  .dshsc-chip { box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
1346
1428
  height: 24px; padding: 0 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;
1347
1429
  background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-family: inherit;
@@ -1365,40 +1447,40 @@ var CSS = `
1365
1447
  .dshsc-chip.legacy { color: var(--dsw-alias-label-tertiary); background: var(--dsw-alias-bg-layer-2); }
1366
1448
  .dshsc-chip.muted { color: var(--dsw-alias-label-tertiary); cursor: default; opacity: .8; }
1367
1449
 
1368
- .dshsc-policy-pop { position: fixed; z-index: 70; box-sizing: border-box; width: min(380px, calc(100vw - 16px));
1369
- max-height: min(600px, calc(100vh - 16px)); overflow: auto; padding: 14px;
1450
+ /* 锚定浮层外壳:介绍浮层等共用(fixed + portal 到宿主弹窗),不再和调用策略弹窗共用 */
1451
+ .dshsc-floatpop { position: fixed; z-index: 70; box-sizing: border-box; width: min(380px, calc(100vw - 16px));
1452
+ max-height: min(600px, calc(100vh - 16px)); overflow: hidden; padding: 14px;
1370
1453
  background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 14px;
1371
1454
  box-shadow: 0 18px 48px rgba(0,0,0,.32), 0 2px 8px rgba(0,0,0,.16);
1372
1455
  display: flex; flex-direction: column; gap: 10px; }
1373
- .dshsc-policy-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
1374
- .dshsc-policy-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); }
1375
- .dshsc-policy-sub { margin: 2px 0 0; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
1376
- .dshsc-policy-close { flex: none; width: 24px; height: 24px; margin: -4px -4px 0 0; border: 0; border-radius: 6px;
1456
+ .dshsc-floatpop-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; flex: none; }
1457
+ .dshsc-floatpop-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); }
1458
+ .dshsc-floatpop-sub { margin: 2px 0 0; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
1459
+ .dshsc-floatpop-close { flex: none; width: 24px; height: 24px; margin: -4px -4px 0 0; border: 0; border-radius: 6px;
1377
1460
  background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; cursor: pointer; }
1378
- .dshsc-policy-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1379
- .dshsc-policy-options { display: flex; flex-direction: column; gap: 6px; }
1380
- .dshsc-policyopt { box-sizing: border-box; display: flex; align-items: flex-start; gap: 10px; width: 100%;
1381
- padding: 9px 10px; text-align: left; border: 1px solid var(--dsw-alias-border-l1); border-radius: 9px;
1382
- background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: inherit; cursor: pointer; }
1383
- .dshsc-policyopt:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
1384
- .dshsc-policyopt.active { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, var(--dsw-alias-border-l1));
1385
- background: color-mix(in srgb, var(--dsw-alias-brand-primary) 7%, transparent); }
1461
+ .dshsc-floatpop-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1462
+
1463
+ /* 调用策略:标准居中弹窗(Overlay)里的单选列表;行距 / 圆点尺寸对齐共享设置的 .dshsc-check */
1464
+ .dshsc-policy-options { max-height: none; overflow: visible; }
1465
+ .dshsc-policyopt { box-sizing: border-box; display: flex; align-items: flex-start; gap: 9px; width: 100%;
1466
+ padding: 5px 8px; text-align: left; border: 0; border-radius: 8px;
1467
+ background: transparent; color: var(--dsw-alias-label-primary); font-family: inherit; cursor: pointer; }
1468
+ .dshsc-policyopt:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); }
1469
+ .dshsc-policyopt:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
1386
1470
  .dshsc-policyopt:disabled { cursor: not-allowed; opacity: .55; }
1387
1471
  .dshsc-policyopt.static { cursor: default; }
1388
- .dshsc-policyopt-dot { flex: none; width: 14px; height: 14px; margin-top: 1px; border-radius: 50%;
1389
- border: 1.5px solid var(--dsw-alias-border-l3); }
1390
- .dshsc-policyopt.active .dshsc-policyopt-dot { border-color: var(--dsw-alias-brand-primary);
1391
- background: radial-gradient(circle, var(--dsw-alias-brand-primary) 0 40%, transparent 47%); }
1472
+ .dshsc-policyopt.static:hover { background: transparent; }
1473
+ .dshsc-policyopt-dot { box-sizing: border-box; flex: none; width: 15px; height: 15px; margin-top: 2px;
1474
+ border-radius: 50%; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); }
1475
+ .dshsc-policyopt.active .dshsc-policyopt-dot { border: 5px solid var(--dsw-alias-brand-primary); }
1392
1476
  .dshsc-policyopt-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1393
- .dshsc-policyopt-body b { font-size: 12px; font-weight: 600; }
1477
+ .dshsc-policyopt-body b { font-size: 13px; font-weight: 600; }
1394
1478
  .dshsc-policyopt-body small { font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary); }
1395
1479
  .dshsc-policy-note { padding: 7px 9px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2);
1396
1480
  color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 15px; }
1397
1481
  .dshsc-policy-note.warn, .dshsc-policy-note.author { color: var(--dsw-alias-state-warn-primary);
1398
1482
  background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 9%, transparent); }
1399
1483
  .dshsc-policy-hidden { display: flex; flex-direction: column; gap: 8px; }
1400
- .dshsc-policy-restore { align-self: flex-start; }
1401
- .dshsc-policy-foot { display: flex; justify-content: flex-end; gap: 6px; }
1402
1484
  .dshsc-policy-confirm { display: flex; flex-direction: column; gap: 6px; padding: 10px; border-radius: 9px;
1403
1485
  border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 34%, var(--dsw-alias-border-l1));
1404
1486
  background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 7%, transparent); }
@@ -1584,6 +1666,26 @@ var CSS = `
1584
1666
  .dshsc-info-sec dd { margin: 0; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
1585
1667
  .dshsc-info-sec dd.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
1586
1668
 
1669
+ /* 列表行的 ✨ 介绍入口:只占一个小胶囊,不改变行高与列宽 */
1670
+ .dshsc-intro-btn { flex: none; display: inline-flex; align-items: center; gap: 3px;
1671
+ height: 19px; padding: 0 7px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;
1672
+ background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px;
1673
+ line-height: 1; cursor: pointer; white-space: nowrap; }
1674
+ .dshsc-intro-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l3);
1675
+ background: var(--dsw-alias-bg-layer-1); }
1676
+ .dshsc-intro-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1677
+ .dshsc-intro-btn .dshsc-spark { font-size: 10px; color: var(--dsw-alias-brand-primary); }
1678
+
1679
+ /* 介绍浮层:复用通用 floatpop 外壳(fixed + portal),只调内容排版 */
1680
+ .dshsc-intro-pop { width: min(360px, calc(100vw - 16px)); gap: 7px; }
1681
+ .dshsc-intro-text { margin: 0; font-size: 12.5px; line-height: 1.7; color: var(--dsw-alias-label-primary);
1682
+ white-space: pre-wrap; overflow-wrap: anywhere; }
1683
+ .dshsc-intro-note { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-tertiary); }
1684
+ .dshsc-intro-meta { margin: 0; font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary); }
1685
+ .dshsc-intro-stale { margin: 0; font-size: 11px; line-height: 15px; color: var(--dsw-alias-state-warn-primary); }
1686
+ .dshsc-intro-error { margin: 0; font-size: 11.5px; line-height: 16px; color: var(--dsw-alias-state-error-primary); }
1687
+ .dshsc-intro-foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 3px; }
1688
+
1587
1689
  /* 正文排版:只用 token,不写死深浅色 */
1588
1690
  .dshsc-md { font-size: 13.5px; line-height: 1.72; color: var(--dsw-alias-label-primary); }
1589
1691
  .dshsc-md-h { margin: 20px 0 8px; line-height: 1.35; font-weight: 650; letter-spacing: -.008em; }
@@ -2101,6 +2203,70 @@ function useSkillReader(row) {
2101
2203
  }, []);
2102
2204
  return { state, file, loadFile, retry };
2103
2205
  }
2206
+ function useSkillIntro({ name, lang, stamp, enabled = true, endpoint = null }) {
2207
+ const base = endpoint || `/skills/${encodeURIComponent(name)}/intro`;
2208
+ const [state, setState] = import_react2.default.useState({ loading: true, intro: null, capability: null, error: null });
2209
+ const [generating, setGenerating] = import_react2.default.useState(false);
2210
+ const [genError, setGenError] = import_react2.default.useState(null);
2211
+ const seq = import_react2.default.useRef(0);
2212
+ const controller = import_react2.default.useRef(null);
2213
+ import_react2.default.useEffect(() => {
2214
+ if (!name || !enabled) {
2215
+ setState({ loading: false, intro: null, capability: null, error: null });
2216
+ return;
2217
+ }
2218
+ const ticket = seq.current + 1;
2219
+ seq.current = ticket;
2220
+ setState({ loading: true, intro: null, capability: null, error: null });
2221
+ void (async () => {
2222
+ try {
2223
+ const result = await apiCall("GET", `${base}?lang=${encodeURIComponent(lang || "zh")}`);
2224
+ if (ticket !== seq.current) return;
2225
+ setState({ loading: false, intro: result.intro || null, capability: result.capability || null, error: null });
2226
+ } catch (error) {
2227
+ if (ticket !== seq.current) return;
2228
+ setState({
2229
+ loading: false,
2230
+ intro: null,
2231
+ capability: null,
2232
+ error: { code: error?.code || null, message: error?.message || "读取失败" }
2233
+ });
2234
+ }
2235
+ })();
2236
+ }, [name, lang, stamp, enabled, base]);
2237
+ import_react2.default.useEffect(() => () => {
2238
+ if (controller.current) {
2239
+ controller.current.abort();
2240
+ controller.current = null;
2241
+ }
2242
+ }, [name, lang, base]);
2243
+ const generate = import_react2.default.useCallback(async (force = false) => {
2244
+ if (!name) return null;
2245
+ if (controller.current) return null;
2246
+ const ctrl = new AbortController();
2247
+ controller.current = ctrl;
2248
+ setGenerating(true);
2249
+ setGenError(null);
2250
+ try {
2251
+ const result = await apiCall(
2252
+ "POST",
2253
+ base,
2254
+ { lang: lang || "zh", force: Boolean(force) },
2255
+ { signal: ctrl.signal }
2256
+ );
2257
+ setState({ loading: false, intro: result.intro || null, capability: result.capability || null, error: null });
2258
+ return result;
2259
+ } catch (error) {
2260
+ if (error?.name === "AbortError") return null;
2261
+ setGenError({ code: error?.code || null, message: error?.message || "生成失败" });
2262
+ return null;
2263
+ } finally {
2264
+ if (controller.current === ctrl) controller.current = null;
2265
+ setGenerating(false);
2266
+ }
2267
+ }, [name, lang, base]);
2268
+ return { ...state, generating, genError, generate };
2269
+ }
2104
2270
 
2105
2271
  // client/views/header.js
2106
2272
  function installSpec(plugin) {
@@ -2294,31 +2460,75 @@ function Toolbar({
2294
2460
  }
2295
2461
 
2296
2462
  // client/views/installed.js
2297
- var import_react4 = __toESM(require("react"), 1);
2463
+ var import_react5 = __toESM(require("react"), 1);
2298
2464
 
2299
- // client/views/policy-popover.js
2465
+ // client/views/intro-popover.js
2300
2466
  var import_react3 = __toESM(require("react"), 1);
2301
- function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpark }) {
2302
- const initial = row.mode === "hidden" ? "auto" : row.mode;
2303
- const [mode, setMode] = import_react3.default.useState(initial);
2304
- const [confirming, setConfirming] = import_react3.default.useState(false);
2467
+ var ERROR_KEY = {
2468
+ "error.skills.intro-no-model": "intro.error.noModel",
2469
+ "error.skills.intro-rate-limit": "intro.error.rateLimit",
2470
+ "error.skills.intro-quota": "intro.error.rateLimit",
2471
+ "error.skills.intro-too-large": "intro.error.tooLarge",
2472
+ "error.skills.intro-timeout": "intro.error.timeout",
2473
+ "error.skills.intro-auth": "intro.error.auth",
2474
+ "error.skills.intro-empty": "intro.error.empty",
2475
+ "error.skills.intro-busy": "intro.error.busy",
2476
+ "error.skills.intro-source": "intro.error.source",
2477
+ "error.skills.subscription-not-found": "intro.error.subscription",
2478
+ "error.skills.intro-source-timeout": "intro.error.sourceTimeout",
2479
+ "error.skills.intro-no-source": "intro.error.noSource"
2480
+ };
2481
+ function formatWhen(iso) {
2482
+ if (!iso) return null;
2483
+ const date = new Date(iso);
2484
+ if (Number.isNaN(date.getTime())) return null;
2485
+ const pad = (value) => String(value).padStart(2, "0");
2486
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
2487
+ }
2488
+ function modelLabel(intro) {
2489
+ if (!intro?.model) return "—";
2490
+ return intro.provider ? `${intro.provider}/${intro.model}` : intro.model;
2491
+ }
2492
+ function IntroPopover({
2493
+ t,
2494
+ name,
2495
+ stamp = "",
2496
+ subtitle = null,
2497
+ endpoint = null,
2498
+ capability,
2499
+ anchor,
2500
+ anchorEl,
2501
+ hint = null,
2502
+ viewLabel = null,
2503
+ onClose,
2504
+ onView = null
2505
+ }) {
2305
2506
  const ref = import_react3.default.useRef(null);
2306
2507
  const [pos, setPos] = import_react3.default.useState({ left: 0, top: 0, visibility: "hidden" });
2508
+ const enabled = capability?.enabled !== false;
2509
+ const available = capability?.available !== false;
2510
+ const intro = useSkillIntro({
2511
+ name,
2512
+ lang: t.lang || "zh",
2513
+ stamp,
2514
+ enabled,
2515
+ endpoint
2516
+ });
2307
2517
  useOverlayLayer(true, onClose);
2308
2518
  import_react3.default.useLayoutEffect(() => {
2309
2519
  const node = ref.current;
2310
2520
  if (!node || !anchor || typeof window === "undefined") return;
2311
2521
  const rect = node.getBoundingClientRect();
2312
2522
  const width = rect.width || 360;
2313
- const height = rect.height || 320;
2314
- let left = anchor.left + anchor.width / 2 - width / 2;
2523
+ const height = rect.height || 220;
2524
+ let left = anchor.left;
2315
2525
  left = Math.max(8, Math.min(left, window.innerWidth - width - 8));
2316
2526
  let top = anchor.bottom + 8;
2317
2527
  if (top + height > window.innerHeight - 8) {
2318
2528
  top = Math.max(8, anchor.top - height - 8);
2319
2529
  }
2320
2530
  setPos({ left, top, visibility: "visible" });
2321
- }, [anchor, confirming, mode]);
2531
+ }, [anchor, intro.loading, intro.intro?.text, intro.generating, intro.genError]);
2322
2532
  import_react3.default.useEffect(() => {
2323
2533
  if (typeof document === "undefined") return void 0;
2324
2534
  const onPointerDown = (event) => {
@@ -2330,6 +2540,108 @@ function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpa
2330
2540
  document.addEventListener("pointerdown", onPointerDown, true);
2331
2541
  return () => document.removeEventListener("pointerdown", onPointerDown, true);
2332
2542
  }, [anchorEl, onClose]);
2543
+ const failure = intro.genError || intro.error;
2544
+ const failureKey = failure?.code ? ERROR_KEY[failure.code] || "intro.error.generic" : null;
2545
+ const meta = intro.intro ? t("intro.meta").replace("{model}", modelLabel(intro.intro)).replace("{time}", formatWhen(intro.intro.generatedAt) || "—") : "";
2546
+ const actions = [];
2547
+ if (intro.intro) {
2548
+ actions.push(h("button", {
2549
+ type: "button",
2550
+ key: "regen",
2551
+ className: "dshsc-btn ghost",
2552
+ disabled: intro.generating || !available,
2553
+ onClick: () => {
2554
+ void intro.generate(true);
2555
+ }
2556
+ }, intro.generating ? t("intro.generating") : t("intro.regenerate")));
2557
+ if (onView) {
2558
+ actions.push(h("button", {
2559
+ type: "button",
2560
+ key: "view",
2561
+ className: "dshsc-btn primary",
2562
+ onClick: onView
2563
+ }, viewLabel || t("intro.view")));
2564
+ }
2565
+ } else {
2566
+ actions.push(h("button", {
2567
+ type: "button",
2568
+ key: "gen",
2569
+ className: "dshsc-btn primary",
2570
+ disabled: intro.generating || !available || !enabled,
2571
+ onClick: () => {
2572
+ void intro.generate(false);
2573
+ }
2574
+ }, intro.generating ? t("intro.generating") : t("intro.generate")));
2575
+ }
2576
+ return portalize(h(
2577
+ "div",
2578
+ {
2579
+ ref,
2580
+ className: "dshsc-floatpop dshsc-intro-pop",
2581
+ role: "dialog",
2582
+ "aria-label": `${t("intro.title")} · ${name}`,
2583
+ style: { left: `${pos.left}px`, top: `${pos.top}px`, visibility: pos.visibility }
2584
+ },
2585
+ h(
2586
+ "div",
2587
+ { className: "dshsc-floatpop-head" },
2588
+ h(
2589
+ "div",
2590
+ null,
2591
+ h("div", { className: "dshsc-floatpop-title" }, `✨ ${t("intro.title")}`),
2592
+ h("div", { className: "dshsc-floatpop-sub", title: subtitle || name }, subtitle || name)
2593
+ ),
2594
+ h("button", {
2595
+ type: "button",
2596
+ className: "dshsc-floatpop-close",
2597
+ "aria-label": t("action.close"),
2598
+ onClick: onClose
2599
+ }, "×")
2600
+ ),
2601
+ intro.loading ? h("p", { className: "dshsc-intro-note" }, t("intro.loading")) : intro.intro ? h("p", { className: "dshsc-intro-text" }, intro.intro.text) : h("p", { className: "dshsc-intro-note" }, t("intro.empty")),
2602
+ intro.intro?.stale ? h("p", { className: "dshsc-intro-stale" }, t("intro.stale")) : null,
2603
+ failureKey ? h("p", { className: "dshsc-intro-error" }, t(failureKey)) : null,
2604
+ meta ? h("p", { className: "dshsc-intro-meta" }, meta) : null,
2605
+ available && !intro.intro && !intro.loading ? h("p", { className: "dshsc-intro-meta" }, hint || t("intro.hint")) : null,
2606
+ !available ? h("p", { className: "dshsc-intro-error" }, t("intro.unavailable")) : null,
2607
+ h("div", { className: "dshsc-intro-foot" }, actions)
2608
+ ));
2609
+ }
2610
+
2611
+ // client/views/policy-modal.js
2612
+ var import_react4 = __toESM(require("react"), 1);
2613
+
2614
+ // client/views/overlay.js
2615
+ function Overlay({ t, title, children, footer, onCancel }) {
2616
+ useOverlayLayer(true, onCancel);
2617
+ return portalize(h(
2618
+ "div",
2619
+ { className: "dshsc-overlay", onClick: (event) => {
2620
+ if (event.target === event.currentTarget) onCancel?.();
2621
+ } },
2622
+ h(
2623
+ "div",
2624
+ { className: "dshsc-modal", role: "dialog", "aria-modal": "true" },
2625
+ h("button", {
2626
+ className: "dshsc-modal-close",
2627
+ "aria-label": t("action.cancel"),
2628
+ onClick: (event) => {
2629
+ event.stopPropagation();
2630
+ onCancel?.();
2631
+ }
2632
+ }, "×"),
2633
+ h("h3", null, title),
2634
+ children,
2635
+ h("div", { className: "dshsc-modal-foot" }, footer)
2636
+ )
2637
+ ), overlayHost());
2638
+ }
2639
+
2640
+ // client/views/policy-modal.js
2641
+ function PolicyModal({ t, row, busy, onClose, onSave, onUnpark }) {
2642
+ const initial = row.mode === "hidden" ? "auto" : row.mode;
2643
+ const [mode, setMode] = import_react4.default.useState(initial);
2644
+ const [confirming, setConfirming] = import_react4.default.useState(false);
2333
2645
  const hidden = row.mode === "hidden";
2334
2646
  const author = row.origin === "author" && row.mode === "manual";
2335
2647
  const flat = row.kind === "file";
@@ -2351,7 +2663,8 @@ function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpa
2351
2663
  type: "button",
2352
2664
  key: `policy-${id}`,
2353
2665
  className: `dshsc-policyopt${mode === id ? " active" : ""}`,
2354
- "aria-pressed": String(mode === id),
2666
+ role: "radio",
2667
+ "aria-checked": String(mode === id),
2355
2668
  disabled: busy || flat,
2356
2669
  onClick: () => choose(id)
2357
2670
  },
@@ -2363,95 +2676,85 @@ function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpa
2363
2676
  h("small", null, desc)
2364
2677
  )
2365
2678
  );
2366
- return portalize(h(
2367
- "div",
2679
+ const footer = confirming ? [
2680
+ h("button", {
2681
+ type: "button",
2682
+ key: "cancel",
2683
+ className: "dshsc-btn ghost",
2684
+ disabled: busy,
2685
+ onClick: () => setConfirming(false)
2686
+ }, t("action.cancel")),
2687
+ h("button", {
2688
+ type: "button",
2689
+ key: "confirm",
2690
+ className: "dshsc-btn danger",
2691
+ disabled: busy,
2692
+ onClick: () => onSave("hidden")
2693
+ }, busy ? t("policy.saving") : t("policy.hiddenConfirm"))
2694
+ ] : hidden ? [
2695
+ h("button", { type: "button", key: "cancel", className: "dshsc-btn ghost", onClick: onClose }, t("action.cancel")),
2696
+ h("button", {
2697
+ type: "button",
2698
+ key: "restore",
2699
+ className: "dshsc-btn primary",
2700
+ disabled: busy,
2701
+ onClick: onUnpark
2702
+ }, t("policy.unpark"))
2703
+ ] : [
2704
+ h("button", { type: "button", key: "cancel", className: "dshsc-btn ghost", onClick: onClose }, t("action.cancel")),
2705
+ h("button", {
2706
+ type: "button",
2707
+ key: "save",
2708
+ className: "dshsc-btn primary",
2709
+ disabled: busy || !dirty || flat,
2710
+ onClick: submit
2711
+ }, busy ? t("policy.saving") : t("action.save"))
2712
+ ];
2713
+ return h(
2714
+ Overlay,
2368
2715
  {
2369
- ref,
2370
- className: "dshsc-policy-pop",
2371
- role: "dialog",
2372
- "aria-label": t("policy.title"),
2373
- style: { left: `${pos.left}px`, top: `${pos.top}px`, visibility: pos.visibility }
2716
+ t,
2717
+ title: t("policy.title"),
2718
+ onCancel: onClose,
2719
+ footer
2374
2720
  },
2375
- h(
2376
- "div",
2377
- { className: "dshsc-policy-head" },
2378
- h(
2379
- "div",
2380
- null,
2381
- h("div", { className: "dshsc-policy-title" }, t("policy.title")),
2382
- h("div", { className: "dshsc-policy-sub" }, t("policy.subtitle"))
2383
- ),
2384
- h("button", {
2385
- type: "button",
2386
- className: "dshsc-policy-close",
2387
- "aria-label": t("action.close"),
2388
- onClick: onClose
2389
- }, "×")
2390
- ),
2391
- author ? h("div", { className: "dshsc-policy-note author" }, t("policy.authorHint")) : null,
2392
- row.legacy ? h("div", { className: "dshsc-policy-note" }, t("policy.legacyHint")) : null,
2393
- row.drift ? h("div", { className: "dshsc-policy-note warn" }, t("policy.driftHint")) : null,
2721
+ h("p", { className: "dshsc-sub", key: "sub" }, t("policy.subtitle")),
2722
+ author ? h("div", { className: "dshsc-policy-note author", key: "author" }, t("policy.authorHint")) : null,
2723
+ row.legacy ? h("div", { className: "dshsc-policy-note", key: "legacy" }, t("policy.legacyHint")) : null,
2724
+ row.drift ? h("div", { className: "dshsc-policy-note warn", key: "drift" }, t("policy.driftHint")) : null,
2394
2725
  hidden ? h(
2395
2726
  "div",
2396
- { className: "dshsc-policy-hidden" },
2727
+ { className: "dshsc-policy-hidden", key: "hidden" },
2397
2728
  h(
2398
2729
  "div",
2399
- { className: "dshsc-policyopt active static" },
2400
- h("span", { className: "dshsc-policyopt-dot" }),
2730
+ { className: "dshsc-panel" },
2401
2731
  h(
2402
- "span",
2403
- { className: "dshsc-policyopt-body" },
2404
- h("b", null, t("policy.hidden")),
2405
- h("small", null, t("policy.hiddenDesc"))
2732
+ "div",
2733
+ { className: "dshsc-policyopt active static", role: "radio", "aria-checked": "true" },
2734
+ h("span", { className: "dshsc-policyopt-dot", "aria-hidden": "true" }),
2735
+ h(
2736
+ "span",
2737
+ { className: "dshsc-policyopt-body" },
2738
+ h("b", null, t("policy.hidden")),
2739
+ h("small", null, t("policy.hiddenDesc"))
2740
+ )
2406
2741
  )
2407
2742
  ),
2408
- h("button", {
2409
- type: "button",
2410
- className: "dshsc-btn primary dshsc-policy-restore",
2411
- disabled: busy,
2412
- onClick: onUnpark
2413
- }, t("policy.unpark")),
2414
- h("p", { className: "dshsc-policy-sub" }, t("policy.hiddenHint"))
2743
+ h("p", { className: "dshsc-sub" }, t("policy.hiddenHint"))
2415
2744
  ) : h(
2416
2745
  "div",
2417
- { className: "dshsc-policy-options" },
2746
+ { className: "dshsc-panel dshsc-policy-options", key: "options", role: "radiogroup", "aria-label": t("policy.title") },
2418
2747
  option("auto", t("policy.auto"), t("policy.autoDesc")),
2419
2748
  option("manual", t("policy.manual"), t("policy.manualDesc")),
2420
2749
  option("hidden", t("policy.hidden"), t("policy.hiddenDesc"))
2421
2750
  ),
2422
2751
  confirming ? h(
2423
2752
  "div",
2424
- { className: "dshsc-policy-confirm" },
2753
+ { className: "dshsc-policy-confirm", key: "confirm" },
2425
2754
  h("b", null, t("policy.hiddenConfirmTitle")),
2426
- h("p", null, t("policy.hiddenConfirmBody")),
2427
- h(
2428
- "div",
2429
- { className: "dshsc-policy-foot" },
2430
- h("button", {
2431
- type: "button",
2432
- className: "dshsc-btn ghost",
2433
- disabled: busy,
2434
- onClick: () => setConfirming(false)
2435
- }, t("action.cancel")),
2436
- h("button", {
2437
- type: "button",
2438
- className: "dshsc-btn danger",
2439
- disabled: busy,
2440
- onClick: () => onSave("hidden")
2441
- }, busy ? t("policy.saving") : t("policy.hiddenConfirm"))
2442
- )
2443
- ) : h(
2444
- "div",
2445
- { className: "dshsc-policy-foot" },
2446
- h("button", { type: "button", className: "dshsc-btn ghost", onClick: onClose }, t("action.cancel")),
2447
- hidden ? null : h("button", {
2448
- type: "button",
2449
- className: "dshsc-btn primary",
2450
- disabled: busy || !dirty || flat,
2451
- onClick: submit
2452
- }, busy ? t("policy.saving") : t("action.save"))
2453
- )
2454
- ), overlayHost());
2755
+ h("p", null, t("policy.hiddenConfirmBody"))
2756
+ ) : null
2757
+ );
2455
2758
  }
2456
2759
 
2457
2760
  // client/views/installed.js
@@ -2473,24 +2776,33 @@ function InstalledTab({
2473
2776
  actions
2474
2777
  }) {
2475
2778
  const updatable = (row) => UPDATABLE.has(row.status);
2476
- const [policy, setPolicy] = import_react4.default.useState(null);
2779
+ const [policy, setPolicy] = import_react5.default.useState(null);
2780
+ const [intro, setIntro] = import_react5.default.useState(null);
2477
2781
  const sharedTargets = new Set((data?.agents?.detected || []).filter((agent) => agent.kind === "agent" && agent.sharedHome === "yes").map((agent) => agent.target));
2478
- import_react4.default.useEffect(() => {
2782
+ import_react5.default.useEffect(() => {
2479
2783
  if (!policy) return;
2480
2784
  if (rows.some((row) => row.name === policy.row.name)) return;
2481
2785
  setPolicy(null);
2482
2786
  }, [policy, rows]);
2483
- function openPolicy(event, row) {
2484
- if (busy !== null || row.kind === "file") return;
2485
- if (row.mode === "manual" && row.origin === "author") return;
2787
+ import_react5.default.useEffect(() => {
2788
+ if (!intro) return;
2789
+ if (rows.some((row) => row.name === intro.row.name)) return;
2790
+ setIntro(null);
2791
+ }, [intro, rows]);
2792
+ function openIntro(event, row) {
2486
2793
  const el = event.currentTarget;
2487
2794
  const rect = el.getBoundingClientRect();
2488
- setPolicy({
2795
+ setIntro({
2489
2796
  row,
2490
2797
  anchorEl: el,
2491
2798
  anchor: { left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }
2492
2799
  });
2493
2800
  }
2801
+ function openPolicy(row) {
2802
+ if (busy !== null || row.kind === "file") return;
2803
+ if (row.mode === "manual" && row.origin === "author") return;
2804
+ setPolicy({ row });
2805
+ }
2494
2806
  function policyChip(row) {
2495
2807
  if (row.status === "unmanaged") {
2496
2808
  return h("span", { className: "dshsc-chip muted", title: t("adopt.switchHint") }, t("policy.unmanaged"));
@@ -2506,7 +2818,7 @@ function InstalledTab({
2506
2818
  disabled: busy !== null,
2507
2819
  title: t("policy.hiddenHint"),
2508
2820
  "aria-haspopup": "dialog",
2509
- onClick: (event) => openPolicy(event, row)
2821
+ onClick: () => openPolicy(row)
2510
2822
  }, t("policy.hidden"));
2511
2823
  }
2512
2824
  if (mode === "manual" && row.origin === "author") {
@@ -2522,7 +2834,7 @@ function InstalledTab({
2522
2834
  disabled: busy !== null,
2523
2835
  title: t("policy.driftHint"),
2524
2836
  "aria-haspopup": "dialog",
2525
- onClick: (event) => openPolicy(event, row)
2837
+ onClick: () => openPolicy(row)
2526
2838
  }, t("policy.drift"));
2527
2839
  }
2528
2840
  if (row.legacy === "dsh-off") {
@@ -2532,7 +2844,7 @@ function InstalledTab({
2532
2844
  disabled: busy !== null,
2533
2845
  title: t("policy.legacyHint"),
2534
2846
  "aria-haspopup": "dialog",
2535
- onClick: (event) => openPolicy(event, row)
2847
+ onClick: () => openPolicy(row)
2536
2848
  }, t("policy.legacy"));
2537
2849
  }
2538
2850
  if (mode === "manual") {
@@ -2542,7 +2854,7 @@ function InstalledTab({
2542
2854
  disabled: busy !== null,
2543
2855
  title: t("policy.manualOnlyHint"),
2544
2856
  "aria-haspopup": "dialog",
2545
- onClick: (event) => openPolicy(event, row)
2857
+ onClick: () => openPolicy(row)
2546
2858
  }, t("policy.manual"));
2547
2859
  }
2548
2860
  return h("button", {
@@ -2551,7 +2863,7 @@ function InstalledTab({
2551
2863
  disabled: busy !== null,
2552
2864
  title: t("policy.open"),
2553
2865
  "aria-haspopup": "dialog",
2554
- onClick: (event) => openPolicy(event, row)
2866
+ onClick: () => openPolicy(row)
2555
2867
  }, t("policy.auto"));
2556
2868
  }
2557
2869
  function statusTag(row) {
@@ -2673,6 +2985,18 @@ function InstalledTab({
2673
2985
  "aria-haspopup": "dialog",
2674
2986
  onClick: () => actions.onView(row)
2675
2987
  }, row.name),
2988
+ data?.intro ? h(
2989
+ "button",
2990
+ {
2991
+ type: "button",
2992
+ className: "dshsc-intro-btn",
2993
+ title: t("intro.open"),
2994
+ "aria-haspopup": "dialog",
2995
+ onClick: (event) => openIntro(event, row)
2996
+ },
2997
+ h("span", { className: "dshsc-spark", "aria-hidden": "true" }, "✨"),
2998
+ t("intro.button")
2999
+ ) : null,
2676
3000
  statusTag(row)
2677
3001
  ),
2678
3002
  sourceLine(row),
@@ -2786,6 +3110,7 @@ function InstalledTab({
2786
3110
  }, dot, h("span", null, label), h("span", { className: "n" }, counts[id]));
2787
3111
  }
2788
3112
  function listBar() {
3113
+ const updateDot = counts.update > 0 ? h("i", { className: "dshsc-fdot" }) : null;
2789
3114
  return h(
2790
3115
  "div",
2791
3116
  { className: "dshsc-listbar" },
@@ -2793,7 +3118,7 @@ function InstalledTab({
2793
3118
  "div",
2794
3119
  { className: "dshsc-filters" },
2795
3120
  filterChip("all", t("filter.all")),
2796
- filterChip("update", t("filter.update"), h("i", { className: "dshsc-fdot" })),
3121
+ filterChip("update", t("filter.update"), updateDot),
2797
3122
  filterChip("unknown", t("filter.unknown")),
2798
3123
  filterChip("manual", t("filter.manual")),
2799
3124
  filterChip("off", t("filter.off"))
@@ -2921,11 +3246,23 @@ function InstalledTab({
2921
3246
  )
2922
3247
  );
2923
3248
  }
2924
- const popover = policy ? h(PolicyPopover, {
3249
+ const introPopover = intro && data?.intro ? h(IntroPopover, {
3250
+ t,
3251
+ name: intro.row.name,
3252
+ stamp: [intro.row.commit || "", intro.row.currentHash || "", intro.row.updatedAt || "", intro.row.status || ""].join("|"),
3253
+ anchor: intro.anchor,
3254
+ anchorEl: intro.anchorEl,
3255
+ capability: data.intro,
3256
+ onClose: () => setIntro(null),
3257
+ onView: () => {
3258
+ const target = intro.row;
3259
+ setIntro(null);
3260
+ actions.onView(target);
3261
+ }
3262
+ }) : null;
3263
+ const policyModal = policy ? h(PolicyModal, {
2925
3264
  t,
2926
3265
  row: policy.row,
2927
- anchor: policy.anchor,
2928
- anchorEl: policy.anchorEl,
2929
3266
  busy: busy !== null,
2930
3267
  onClose: () => setPolicy(null),
2931
3268
  onSave: (mode) => {
@@ -2939,7 +3276,7 @@ function InstalledTab({
2939
3276
  actions.onRequestUnpark(target);
2940
3277
  }
2941
3278
  }) : null;
2942
- return h(import_react4.default.Fragment, null, content(), popover);
3279
+ return h(import_react5.default.Fragment, null, content(), policyModal, introPopover);
2943
3280
  }
2944
3281
 
2945
3282
  // client/views/agent-tip.js
@@ -2974,10 +3311,14 @@ function AgentTip({ t, agentTip, agentTipPos, nodeRef }) {
2974
3311
  }
2975
3312
 
2976
3313
  // client/views/subscriptions.js
3314
+ var import_react6 = __toESM(require("react"), 1);
2977
3315
  function repoOf(source) {
2978
3316
  const value = String(source || "").trim().toLowerCase().replace(/\.git$/, "");
2979
3317
  return value || null;
2980
3318
  }
3319
+ function shortCommit(value) {
3320
+ return typeof value === "string" ? value.slice(0, 7) : "";
3321
+ }
2981
3322
  function SubscriptionsTab({
2982
3323
  t,
2983
3324
  subscriptions,
@@ -2985,12 +3326,72 @@ function SubscriptionsTab({
2985
3326
  expanded,
2986
3327
  busy,
2987
3328
  writable,
3329
+ capability = null,
2988
3330
  onToggle,
2989
3331
  onRefresh,
2990
3332
  onDelete,
2991
3333
  onInstall,
2992
3334
  onAdd
2993
3335
  }) {
3336
+ const [intro, setIntro] = import_react6.default.useState(null);
3337
+ import_react6.default.useEffect(() => {
3338
+ if (!intro) return;
3339
+ const subscription = subscriptions.find((item) => item.id === intro.subscriptionId);
3340
+ const entries = subscription?.snapshot?.entries || [];
3341
+ if (entries.some((entry) => entry.name === intro.name)) return;
3342
+ setIntro(null);
3343
+ }, [intro, subscriptions]);
3344
+ function openIntro(event, subscription, entry) {
3345
+ const el = event.currentTarget;
3346
+ const rect = el.getBoundingClientRect();
3347
+ setIntro({
3348
+ subscriptionId: subscription.id,
3349
+ name: entry.name,
3350
+ anchorEl: el,
3351
+ anchor: { left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }
3352
+ });
3353
+ }
3354
+ function introButton(subscription, entry) {
3355
+ return h(
3356
+ "button",
3357
+ {
3358
+ type: "button",
3359
+ className: "dshsc-intro-btn",
3360
+ title: t("intro.open"),
3361
+ "aria-haspopup": "dialog",
3362
+ onClick: (clickEvent) => openIntro(clickEvent, subscription, entry)
3363
+ },
3364
+ h("span", { className: "dshsc-spark", "aria-hidden": "true" }, "✨"),
3365
+ t("intro.button")
3366
+ );
3367
+ }
3368
+ const introSubscription = intro ? subscriptions.find((item) => item.id === intro.subscriptionId) || null : null;
3369
+ const introEntry = introSubscription ? (introSubscription.snapshot?.entries || []).find((entry) => entry.name === intro.name) || null : null;
3370
+ const introPopover = intro && introEntry && capability ? (() => {
3371
+ const commit = introSubscription.snapshot?.commit || "";
3372
+ const installedAlready = rows.some((row) => {
3373
+ if (row.name !== introEntry.name) return false;
3374
+ if (row.subscriptionId === introSubscription.id) return true;
3375
+ return repoOf(row.source) === repoOf(introSubscription.source);
3376
+ });
3377
+ return h(IntroPopover, {
3378
+ t,
3379
+ name: introEntry.name,
3380
+ stamp: commit,
3381
+ subtitle: `${introSubscription.source}@${shortCommit(commit)}`,
3382
+ endpoint: `/subscriptions/${encodeURIComponent(introSubscription.id)}/entries/${encodeURIComponent(introEntry.name)}/intro`,
3383
+ capability,
3384
+ anchor: intro.anchor,
3385
+ anchorEl: intro.anchorEl,
3386
+ hint: t("intro.hintRemote").replace("{source}", introSubscription.source).replace("{commit}", shortCommit(commit) || "—"),
3387
+ viewLabel: t("action.install"),
3388
+ onView: installedAlready ? null : () => {
3389
+ setIntro(null);
3390
+ void onInstall(introSubscription, introEntry);
3391
+ },
3392
+ onClose: () => setIntro(null)
3393
+ });
3394
+ })() : null;
2994
3395
  if (subscriptions.length === 0) {
2995
3396
  return h(
2996
3397
  "div",
@@ -3013,154 +3414,140 @@ function SubscriptionsTab({
3013
3414
  );
3014
3415
  }
3015
3416
  return h(
3016
- "div",
3017
- { className: "dshsc-tabpane" },
3018
- // 表头与仓库行共用同一个滚动容器,位置 sticky;宽屏 4 列,窄屏收成「仓库」/「操作」。
3417
+ import_react6.default.Fragment,
3418
+ null,
3019
3419
  h(
3020
3420
  "div",
3021
- { className: "dshsc-listscroll dshsc-subscroll" },
3421
+ { className: "dshsc-tabpane" },
3422
+ // 表头与仓库行共用同一个滚动容器,位置 sticky;宽屏 4 列,窄屏收成「仓库」/「操作」。
3022
3423
  h(
3023
3424
  "div",
3024
- { className: "dshsc-subhead" },
3025
- h("div", { className: "dshsc-hrepo" }, t("column.repo")),
3026
- h("div", { className: "dshsc-href" }, t("column.ref")),
3027
- h("div", { className: "dshsc-hcount" }, t("column.entries")),
3028
- h("div", { className: "dshsc-hact" }, t("column.actions"))
3029
- ),
3030
- subscriptions.map((subscription) => {
3031
- const entries = subscription.snapshot?.entries || [];
3032
- const repo = repoOf(subscription.source);
3033
- const rowsByName = new Map(rows.map((row) => [row.name, row]));
3034
- const installedRow = (name) => {
3035
- const row = rowsByName.get(name);
3036
- if (!row) return null;
3037
- if (row.subscriptionId === subscription.id) return row;
3038
- return repo && repoOf(row.source) === repo ? row : null;
3039
- };
3040
- const installedCount = entries.filter((entry) => installedRow(entry.name)).length;
3041
- const open = Boolean(expanded[subscription.id]);
3042
- return h(
3425
+ { className: "dshsc-listscroll dshsc-subscroll" },
3426
+ h(
3043
3427
  "div",
3044
- { className: "dshsc-sub-block", key: subscription.id },
3045
- h(
3428
+ { className: "dshsc-subhead" },
3429
+ h("div", { className: "dshsc-hrepo" }, t("column.repo")),
3430
+ h("div", { className: "dshsc-href" }, t("column.ref")),
3431
+ h("div", { className: "dshsc-hcount" }, t("column.entries")),
3432
+ h("div", { className: "dshsc-hact" }, t("column.actions"))
3433
+ ),
3434
+ subscriptions.map((subscription) => {
3435
+ const entries = subscription.snapshot?.entries || [];
3436
+ const repo = repoOf(subscription.source);
3437
+ const rowsByName = new Map(rows.map((row) => [row.name, row]));
3438
+ const installedRow = (name) => {
3439
+ const row = rowsByName.get(name);
3440
+ if (!row) return null;
3441
+ if (row.subscriptionId === subscription.id) return row;
3442
+ return repo && repoOf(row.source) === repo ? row : null;
3443
+ };
3444
+ const installedCount = entries.filter((entry) => installedRow(entry.name)).length;
3445
+ const open = Boolean(expanded[subscription.id]);
3446
+ return h(
3046
3447
  "div",
3047
- { className: "dshsc-sub-row" },
3048
- h(
3049
- "div",
3050
- { className: "dshsc-sub-repo" },
3051
- h("button", {
3052
- className: "dshsc-caret",
3053
- "aria-expanded": String(open),
3054
- "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
3055
- onClick: () => onToggle(subscription.id)
3056
- }, h(CaretIcon, { open })),
3057
- h("span", { className: "dshsc-name" }, subscription.source)
3058
- ),
3059
- // 宽屏铺成 Ref / 条目两列;窄屏回到仓库名下方一行。
3448
+ { className: "dshsc-sub-block", key: subscription.id },
3060
3449
  h(
3061
3450
  "div",
3062
- { className: "dshsc-sub-meta" },
3063
- h("span", { className: "dshsc-tag dshsc-subref" }, subscription.ref || t("subscription.defaultRef")),
3451
+ { className: "dshsc-sub-row" },
3452
+ h(
3453
+ "div",
3454
+ { className: "dshsc-sub-repo" },
3455
+ h("button", {
3456
+ className: "dshsc-caret",
3457
+ "aria-expanded": String(open),
3458
+ "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
3459
+ onClick: () => onToggle(subscription.id)
3460
+ }, h(CaretIcon, { open })),
3461
+ h("span", { className: "dshsc-name" }, subscription.source)
3462
+ ),
3463
+ // 宽屏铺成 Ref / 条目两列;窄屏回到仓库名下方一行。
3064
3464
  h(
3065
- "span",
3066
- { className: "dshsc-substat" },
3067
- `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedCount}`
3465
+ "div",
3466
+ { className: "dshsc-sub-meta" },
3467
+ h("span", { className: "dshsc-tag dshsc-subref" }, subscription.ref || t("subscription.defaultRef")),
3468
+ h(
3469
+ "span",
3470
+ { className: "dshsc-substat" },
3471
+ `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedCount}`
3472
+ )
3473
+ ),
3474
+ h(
3475
+ "div",
3476
+ { className: "dshsc-actions" },
3477
+ h("button", {
3478
+ className: "dshsc-btn sq",
3479
+ disabled: busy !== null,
3480
+ title: t("action.checkUpdates"),
3481
+ "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
3482
+ onClick: () => void onRefresh(subscription)
3483
+ }, h(RefreshIcon, { size: 13 })),
3484
+ h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
3068
3485
  )
3069
3486
  ),
3070
- h(
3071
- "div",
3072
- { className: "dshsc-actions" },
3073
- h("button", {
3074
- className: "dshsc-btn sq",
3075
- disabled: busy !== null,
3076
- title: t("action.checkUpdates"),
3077
- "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
3078
- onClick: () => void onRefresh(subscription)
3079
- }, h(RefreshIcon, { size: 13 })),
3080
- h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
3081
- )
3082
- ),
3083
- open ? h(
3084
- "div",
3085
- { className: "dshsc-sub-list" },
3086
- h(
3487
+ open ? h(
3087
3488
  "div",
3088
- { className: "dshsc-entryhead" },
3089
- h("div", null, t("column.skill")),
3090
- h("div", null, t("column.status")),
3091
- h("div", { className: "right" }, t("column.actions"))
3092
- ),
3093
- entries.map((entry) => {
3094
- const installedAlready = Boolean(installedRow(entry.name));
3095
- return h(
3489
+ { className: "dshsc-sub-list" },
3490
+ h(
3096
3491
  "div",
3097
- { className: "dshsc-sub-entry", key: entry.name },
3098
- h("span", { className: "dshsc-name" }, entry.name),
3099
- h(
3100
- "span",
3101
- { className: `dshsc-state${installedAlready ? " ok" : ""}` },
3102
- installedAlready ? "✓ " : "",
3103
- installedAlready ? t("installed") : t("notInstalled")
3104
- ),
3105
- h(
3492
+ { className: "dshsc-entryhead" },
3493
+ h("div", null, t("column.skill")),
3494
+ h("div", null, t("column.status")),
3495
+ h("div", { className: "right" }, t("column.actions"))
3496
+ ),
3497
+ entries.map((entry) => {
3498
+ const installedAlready = Boolean(installedRow(entry.name));
3499
+ return h(
3106
3500
  "div",
3107
- { className: "dshsc-actions" },
3108
- installedAlready ? h("span", { className: "dshsc-noop" }, "-") : h("button", {
3109
- className: "dshsc-btn",
3110
- disabled: !writable || busy !== null,
3111
- onClick: () => void onInstall(subscription, entry)
3112
- }, t("action.install"))
3113
- )
3114
- );
3115
- })
3116
- ) : null
3117
- );
3118
- })
3119
- )
3501
+ { className: "dshsc-sub-entry", key: entry.name },
3502
+ h(
3503
+ "div",
3504
+ { className: "dshsc-sub-skill" },
3505
+ h(
3506
+ "div",
3507
+ { className: "dshsc-sub-namerow" },
3508
+ h("span", { className: "dshsc-name" }, entry.name),
3509
+ capability ? introButton(subscription, entry) : null
3510
+ ),
3511
+ entry.description ? h("div", { className: "dshsc-sub-desc", title: entry.description }, entry.description) : null
3512
+ ),
3513
+ h(
3514
+ "span",
3515
+ { className: `dshsc-state${installedAlready ? " ok" : ""}` },
3516
+ installedAlready ? t("installed") : t("notInstalled")
3517
+ ),
3518
+ h(
3519
+ "div",
3520
+ { className: "dshsc-actions" },
3521
+ installedAlready ? h("span", { className: "dshsc-noop" }, "-") : h("button", {
3522
+ className: "dshsc-btn",
3523
+ disabled: !writable || busy !== null,
3524
+ onClick: () => void onInstall(subscription, entry)
3525
+ }, t("action.install"))
3526
+ )
3527
+ );
3528
+ })
3529
+ ) : null
3530
+ );
3531
+ })
3532
+ )
3533
+ ),
3534
+ introPopover
3120
3535
  );
3121
3536
  }
3122
3537
 
3123
3538
  // client/views/modals.js
3124
- var import_react6 = __toESM(require("react"), 1);
3125
-
3126
- // client/views/add-skill.js
3127
- var import_react5 = __toESM(require("react"), 1);
3128
-
3129
- // client/views/overlay.js
3130
- function Overlay({ t, title, children, footer, onCancel }) {
3131
- useOverlayLayer(true, onCancel);
3132
- return portalize(h(
3133
- "div",
3134
- { className: "dshsc-overlay", onClick: (event) => {
3135
- if (event.target === event.currentTarget) onCancel?.();
3136
- } },
3137
- h(
3138
- "div",
3139
- { className: "dshsc-modal", role: "dialog", "aria-modal": "true" },
3140
- h("button", {
3141
- className: "dshsc-modal-close",
3142
- "aria-label": t("action.cancel"),
3143
- onClick: (event) => {
3144
- event.stopPropagation();
3145
- onCancel?.();
3146
- }
3147
- }, "×"),
3148
- h("h3", null, title),
3149
- children,
3150
- h("div", { className: "dshsc-modal-foot" }, footer)
3151
- )
3152
- ), overlayHost());
3153
- }
3539
+ var import_react8 = __toESM(require("react"), 1);
3154
3540
 
3155
3541
  // client/views/add-skill.js
3542
+ var import_react7 = __toESM(require("react"), 1);
3156
3543
  function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3157
- const [source, setSource] = import_react5.default.useState("");
3158
- const [phase, setPhase] = import_react5.default.useState("idle");
3159
- const [options, setOptions] = import_react5.default.useState([]);
3160
- const [selected, setSelected] = import_react5.default.useState(() => /* @__PURE__ */ new Set());
3161
- const [manual, setManual] = import_react5.default.useState("");
3162
- const [skipped, setSkipped] = import_react5.default.useState(0);
3163
- const [error, setError] = import_react5.default.useState(null);
3544
+ const [source, setSource] = import_react7.default.useState("");
3545
+ const [phase, setPhase] = import_react7.default.useState("idle");
3546
+ const [options, setOptions] = import_react7.default.useState([]);
3547
+ const [selected, setSelected] = import_react7.default.useState(() => /* @__PURE__ */ new Set());
3548
+ const [manual, setManual] = import_react7.default.useState("");
3549
+ const [skipped, setSkipped] = import_react7.default.useState(0);
3550
+ const [error, setError] = import_react7.default.useState(null);
3164
3551
  const reset = (value) => {
3165
3552
  setSource(value);
3166
3553
  setPhase("idle");
@@ -3296,7 +3683,7 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3296
3683
 
3297
3684
  // client/views/modals.js
3298
3685
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
3299
- const [form, setForm] = import_react6.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
3686
+ const [form, setForm] = import_react8.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
3300
3687
  const incomplete = fields.some(
3301
3688
  (field) => field.required !== false && !String(form[field.key] || "").trim()
3302
3689
  );
@@ -3344,7 +3731,7 @@ function ExportsModal({ t, row, agents, onCancel, onSave }) {
3344
3731
  const candidates = agents.filter(
3345
3732
  (agent) => agent.kind === "agent" && agent.exists && agent.exportable
3346
3733
  );
3347
- const [targets, setTargets] = import_react6.default.useState(() => {
3734
+ const [targets, setTargets] = import_react8.default.useState(() => {
3348
3735
  const available = new Set(candidates.map((agent) => agent.target));
3349
3736
  return new Set((row.exports || []).map((item) => item.target).filter((target) => available.has(target)));
3350
3737
  });
@@ -3386,7 +3773,7 @@ function ExportsModal({ t, row, agents, onCancel, onSave }) {
3386
3773
  );
3387
3774
  }
3388
3775
  function DefaultExportModal({ t, settings, agents, onCancel, onSave }) {
3389
- const [targets, setTargets] = import_react6.default.useState(() => new Set(
3776
+ const [targets, setTargets] = import_react8.default.useState(() => new Set(
3390
3777
  Array.isArray(settings.exportTargets) ? settings.exportTargets : []
3391
3778
  ));
3392
3779
  const toggle = (target) => setTargets((prev) => {
@@ -3515,7 +3902,7 @@ ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
3515
3902
  }
3516
3903
 
3517
3904
  // client/views/skill-reader.js
3518
- var import_react7 = __toESM(require("react"), 1);
3905
+ var import_react9 = __toESM(require("react"), 1);
3519
3906
 
3520
3907
  // client/markdown.js
3521
3908
  function classifyHref(href) {
@@ -3951,7 +4338,7 @@ function formatBytes(bytes) {
3951
4338
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3952
4339
  return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3953
4340
  }
3954
- function formatWhen(iso) {
4341
+ function formatWhen2(iso) {
3955
4342
  if (!iso) return null;
3956
4343
  const date = new Date(iso);
3957
4344
  if (Number.isNaN(date.getTime())) return null;
@@ -4011,37 +4398,37 @@ function SkillReader({
4011
4398
  onToast
4012
4399
  }) {
4013
4400
  const { state, file, loadFile, retry } = useSkillReader(row);
4014
- const [tab, setTab] = import_react7.default.useState(initialTab);
4015
- const [format, setFormat] = import_react7.default.useState("rendered");
4016
- const [docOverride, setDocOverride] = import_react7.default.useState(null);
4017
- const [fileFormat, setFileFormat] = import_react7.default.useState("rendered");
4018
- const [tocActive, setTocActive] = import_react7.default.useState(null);
4019
- const [descOpen, setDescOpen] = import_react7.default.useState(false);
4020
- const [descClamped, setDescClamped] = import_react7.default.useState(false);
4021
- const bodyRef = import_react7.default.useRef(null);
4022
- const dialogRef = import_react7.default.useRef(null);
4023
- const descRef = import_react7.default.useRef(null);
4024
- const scrollRef = import_react7.default.useRef(0);
4025
- useOverlayLayer(variant === "drawer" || variant === "page", onClose);
4026
4401
  const name = row?.name || "";
4402
+ const [tab, setTab] = import_react9.default.useState(initialTab);
4403
+ const [format, setFormat] = import_react9.default.useState("rendered");
4404
+ const [docOverride, setDocOverride] = import_react9.default.useState(null);
4405
+ const [fileFormat, setFileFormat] = import_react9.default.useState("rendered");
4406
+ const [tocActive, setTocActive] = import_react9.default.useState(null);
4407
+ const [descOpen, setDescOpen] = import_react9.default.useState(false);
4408
+ const [descClamped, setDescClamped] = import_react9.default.useState(false);
4409
+ const bodyRef = import_react9.default.useRef(null);
4410
+ const dialogRef = import_react9.default.useRef(null);
4411
+ const descRef = import_react9.default.useRef(null);
4412
+ const scrollRef = import_react9.default.useRef(0);
4413
+ useOverlayLayer(variant === "drawer" || variant === "page", onClose);
4027
4414
  const tree = state.tree;
4028
4415
  const files = tree?.files || [];
4029
4416
  const fileCount = files.filter((entry) => entry.kind === "file").length;
4030
4417
  const bodyText = docOverride ? docOverride.content : state.body?.content || "";
4031
4418
  const description = row?.description || state.body?.meta?.description || "";
4032
4419
  const headings = tab === "doc" && bodyText ? parseCached(bodyText).headings : [];
4033
- import_react7.default.useEffect(() => {
4420
+ import_react9.default.useEffect(() => {
4034
4421
  setDocOverride(null);
4035
4422
  scrollRef.current = 0;
4036
4423
  }, [name]);
4037
- const descOwnerRef = import_react7.default.useRef(name);
4038
- import_react7.default.useEffect(() => {
4424
+ const descOwnerRef = import_react9.default.useRef(name);
4425
+ import_react9.default.useEffect(() => {
4039
4426
  if (descOwnerRef.current === name) return;
4040
4427
  descOwnerRef.current = name;
4041
4428
  setDescOpen(false);
4042
4429
  setDescClamped(false);
4043
4430
  }, [name]);
4044
- import_react7.default.useLayoutEffect(() => {
4431
+ import_react9.default.useLayoutEffect(() => {
4045
4432
  const node = descRef.current;
4046
4433
  if (!node) return void 0;
4047
4434
  const measure = () => {
@@ -4063,16 +4450,16 @@ function SkillReader({
4063
4450
  window.removeEventListener("resize", measure);
4064
4451
  };
4065
4452
  }, [description, descOpen]);
4066
- import_react7.default.useEffect(() => {
4453
+ import_react9.default.useEffect(() => {
4067
4454
  if (!initialFile) return;
4068
4455
  setTab("files");
4069
4456
  void loadFile(initialFile);
4070
4457
  }, [initialFile, loadFile]);
4071
- import_react7.default.useEffect(() => {
4458
+ import_react9.default.useEffect(() => {
4072
4459
  const node = bodyRef.current;
4073
4460
  if (node && scrollRef.current) node.scrollTop = scrollRef.current;
4074
4461
  });
4075
- import_react7.default.useEffect(() => {
4462
+ import_react9.default.useEffect(() => {
4076
4463
  if (variant !== "drawer" || typeof document === "undefined") return void 0;
4077
4464
  const previous = document.activeElement;
4078
4465
  const node = dialogRef.current;
@@ -4099,12 +4486,12 @@ function SkillReader({
4099
4486
  previous?.focus?.();
4100
4487
  };
4101
4488
  }, [variant, onClose]);
4102
- import_react7.default.useEffect(() => {
4489
+ import_react9.default.useEffect(() => {
4103
4490
  if (variant !== "page" || typeof document === "undefined") return;
4104
4491
  const node = document.getElementById("dshsc-reader-name");
4105
4492
  node?.focus?.();
4106
4493
  }, [variant, name]);
4107
- import_react7.default.useEffect(() => {
4494
+ import_react9.default.useEffect(() => {
4108
4495
  if (variant !== "page" || typeof window === "undefined") return void 0;
4109
4496
  const node = bodyRef.current;
4110
4497
  if (!node?.querySelectorAll) return void 0;
@@ -4200,7 +4587,7 @@ function SkillReader({
4200
4587
  h("span", { className: "dshsc-code" }, row?.source ? `${row.source}${row.commit ? `#${short(row.commit)}` : row.ref ? `#${row.ref}` : ""}` : t("meta.noUpstream"))
4201
4588
  ),
4202
4589
  tree ? h("span", null, t("viewer.fileCount").replace("{n}", String(fileCount))) : null,
4203
- row?.updatedAt ? h("span", null, formatWhen(row.updatedAt)) : null
4590
+ row?.updatedAt ? h("span", null, formatWhen2(row.updatedAt)) : null
4204
4591
  )
4205
4592
  );
4206
4593
  const tabsBar = h(
@@ -4417,8 +4804,8 @@ function SkillReader({
4417
4804
  ["viewer.info.hash", hash ? h("span", { className: "mono" }, hash) : t("viewer.info.none"), "mono"]
4418
4805
  ];
4419
4806
  const localRows = [
4420
- ["viewer.info.installedAt", formatWhen(row?.installedAt) || t("viewer.info.none")],
4421
- ["viewer.info.updatedAt", formatWhen(row?.updatedAt) || t("viewer.info.none")],
4807
+ ["viewer.info.installedAt", formatWhen2(row?.installedAt) || t("viewer.info.none")],
4808
+ ["viewer.info.updatedAt", formatWhen2(row?.updatedAt) || t("viewer.info.none")],
4422
4809
  ["viewer.info.size", tree ? `${formatBytes(tree.totalBytes)} · ${t("viewer.fileCount").replace("{n}", String(fileCount))}` : t("viewer.info.none")],
4423
4810
  ["viewer.info.localChange", row?.status === "locally-modified" || row?.status === "conflict" ? t("viewer.info.localChangeYes") : t("viewer.info.localChangeNone")]
4424
4811
  ];
@@ -4542,19 +4929,19 @@ function SkillReader({
4542
4929
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
4543
4930
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4544
4931
  const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
4545
- const [tab, setTab] = import_react8.default.useState("installed");
4546
- const [view, setView] = import_react8.default.useState("flat");
4547
- const [modal, setModal] = import_react8.default.useState(null);
4548
- const [expanded, setExpanded] = import_react8.default.useState({});
4549
- const [placementBusy, setPlacementBusy] = import_react8.default.useState(null);
4550
- const [placementOverride, setPlacementOverride] = import_react8.default.useState(null);
4551
- const [query, setQuery] = import_react8.default.useState("");
4552
- const [viewer, setViewer] = import_react8.default.useState(null);
4553
- const rootRef = import_react8.default.useRef(null);
4554
- const scrollPos = import_react8.default.useRef(null);
4555
- const restoreScroll = import_react8.default.useRef(null);
4556
- const [filter, setFilter] = import_react8.default.useState("all");
4557
- const [builtinOpen, setBuiltinOpen] = import_react8.default.useState(false);
4932
+ const [tab, setTab] = import_react10.default.useState("installed");
4933
+ const [view, setView] = import_react10.default.useState("flat");
4934
+ const [modal, setModal] = import_react10.default.useState(null);
4935
+ const [expanded, setExpanded] = import_react10.default.useState({});
4936
+ const [placementBusy, setPlacementBusy] = import_react10.default.useState(null);
4937
+ const [placementOverride, setPlacementOverride] = import_react10.default.useState(null);
4938
+ const [query, setQuery] = import_react10.default.useState("");
4939
+ const [viewer, setViewer] = import_react10.default.useState(null);
4940
+ const rootRef = import_react10.default.useRef(null);
4941
+ const scrollPos = import_react10.default.useRef(null);
4942
+ const restoreScroll = import_react10.default.useRef(null);
4943
+ const [filter, setFilter] = import_react10.default.useState("all");
4944
+ const [builtinOpen, setBuiltinOpen] = import_react10.default.useState(false);
4558
4945
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
4559
4946
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
4560
4947
  const rows = data?.installs || [];
@@ -4577,7 +4964,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4577
4964
  const agentLabels = row.exports.map((item) => item.label);
4578
4965
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
4579
4966
  });
4580
- import_react8.default.useEffect(() => {
4967
+ import_react10.default.useEffect(() => {
4581
4968
  if (!viewer || !data) return;
4582
4969
  if (rows.some((row) => row.name === viewer.name)) return;
4583
4970
  setViewer(null);
@@ -4611,7 +4998,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4611
4998
  if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
4612
4999
  setViewer(null);
4613
5000
  }
4614
- import_react8.default.useLayoutEffect(() => {
5001
+ import_react10.default.useLayoutEffect(() => {
4615
5002
  if (restoreScroll.current === null) return;
4616
5003
  const target = restoreScroll.current;
4617
5004
  restoreScroll.current = null;
@@ -4680,7 +5067,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4680
5067
  onToast: (next) => setToast(next)
4681
5068
  }) : null;
4682
5069
  const listNode = h(
4683
- import_react8.default.Fragment,
5070
+ import_react10.default.Fragment,
4684
5071
  null,
4685
5072
  h(Header, {
4686
5073
  t,
@@ -4738,6 +5125,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4738
5125
  expanded,
4739
5126
  busy,
4740
5127
  writable,
5128
+ capability: data?.intro || null,
4741
5129
  ...subscriptionActions
4742
5130
  }),
4743
5131
  busy ? h("div", { className: "dshsc-meta" }, `${t("busy.working")} ${busy}`) : null