@morehao/dsh-skills-console 0.5.0 → 0.5.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.
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) {
@@ -1307,6 +1365,11 @@ var CSS = `
1307
1365
  align-items: center; font-size: 12px; padding: 6px 0; border-top: 1px solid var(--dsw-alias-border-l1); }
1308
1366
  .dshsc-entryhead + .dshsc-sub-entry { border-top: 0; }
1309
1367
  .dshsc-sub-entry .dshsc-name { font-size: 12px; font-weight: 500; }
1368
+ /* 条目第一列:名字 + ✨ 同一行,作者原描述单独一行(超长省略,完整内容在 title) */
1369
+ .dshsc-sub-skill { min-width: 0; }
1370
+ .dshsc-sub-namerow { display: flex; align-items: center; gap: 6px; min-width: 0; }
1371
+ .dshsc-sub-desc { margin-top: 1px; font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary);
1372
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1310
1373
  .dshsc-state { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
1311
1374
  .dshsc-state.ok svg { color: var(--dsw-alias-state-success-primary); }
1312
1375
  .dshsc-sub-entry .dshsc-actions { justify-self: end; display: flex; justify-content: flex-end; }
@@ -1584,6 +1647,26 @@ var CSS = `
1584
1647
  .dshsc-info-sec dd { margin: 0; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
1585
1648
  .dshsc-info-sec dd.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
1586
1649
 
1650
+ /* 列表行的 ✨ 介绍入口:只占一个小胶囊,不改变行高与列宽 */
1651
+ .dshsc-intro-btn { flex: none; display: inline-flex; align-items: center; gap: 3px;
1652
+ height: 19px; padding: 0 7px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;
1653
+ background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px;
1654
+ line-height: 1; cursor: pointer; white-space: nowrap; }
1655
+ .dshsc-intro-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l3);
1656
+ background: var(--dsw-alias-bg-layer-1); }
1657
+ .dshsc-intro-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1658
+ .dshsc-intro-btn .dshsc-spark { font-size: 10px; color: var(--dsw-alias-brand-primary); }
1659
+
1660
+ /* 介绍浮层:复用 policy 浮层的外壳(fixed + portal),只调内容排版 */
1661
+ .dshsc-intro-pop { width: min(360px, calc(100vw - 16px)); gap: 7px; }
1662
+ .dshsc-intro-text { margin: 0; font-size: 12.5px; line-height: 1.7; color: var(--dsw-alias-label-primary);
1663
+ white-space: pre-wrap; overflow-wrap: anywhere; }
1664
+ .dshsc-intro-note { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-tertiary); }
1665
+ .dshsc-intro-meta { margin: 0; font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary); }
1666
+ .dshsc-intro-stale { margin: 0; font-size: 11px; line-height: 15px; color: var(--dsw-alias-state-warn-primary); }
1667
+ .dshsc-intro-error { margin: 0; font-size: 11.5px; line-height: 16px; color: var(--dsw-alias-state-error-primary); }
1668
+ .dshsc-intro-foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 3px; }
1669
+
1587
1670
  /* 正文排版:只用 token,不写死深浅色 */
1588
1671
  .dshsc-md { font-size: 13.5px; line-height: 1.72; color: var(--dsw-alias-label-primary); }
1589
1672
  .dshsc-md-h { margin: 20px 0 8px; line-height: 1.35; font-weight: 650; letter-spacing: -.008em; }
@@ -2101,6 +2184,70 @@ function useSkillReader(row) {
2101
2184
  }, []);
2102
2185
  return { state, file, loadFile, retry };
2103
2186
  }
2187
+ function useSkillIntro({ name, lang, stamp, enabled = true, endpoint = null }) {
2188
+ const base = endpoint || `/skills/${encodeURIComponent(name)}/intro`;
2189
+ const [state, setState] = import_react2.default.useState({ loading: true, intro: null, capability: null, error: null });
2190
+ const [generating, setGenerating] = import_react2.default.useState(false);
2191
+ const [genError, setGenError] = import_react2.default.useState(null);
2192
+ const seq = import_react2.default.useRef(0);
2193
+ const controller = import_react2.default.useRef(null);
2194
+ import_react2.default.useEffect(() => {
2195
+ if (!name || !enabled) {
2196
+ setState({ loading: false, intro: null, capability: null, error: null });
2197
+ return;
2198
+ }
2199
+ const ticket = seq.current + 1;
2200
+ seq.current = ticket;
2201
+ setState({ loading: true, intro: null, capability: null, error: null });
2202
+ void (async () => {
2203
+ try {
2204
+ const result = await apiCall("GET", `${base}?lang=${encodeURIComponent(lang || "zh")}`);
2205
+ if (ticket !== seq.current) return;
2206
+ setState({ loading: false, intro: result.intro || null, capability: result.capability || null, error: null });
2207
+ } catch (error) {
2208
+ if (ticket !== seq.current) return;
2209
+ setState({
2210
+ loading: false,
2211
+ intro: null,
2212
+ capability: null,
2213
+ error: { code: error?.code || null, message: error?.message || "读取失败" }
2214
+ });
2215
+ }
2216
+ })();
2217
+ }, [name, lang, stamp, enabled, base]);
2218
+ import_react2.default.useEffect(() => () => {
2219
+ if (controller.current) {
2220
+ controller.current.abort();
2221
+ controller.current = null;
2222
+ }
2223
+ }, [name, lang, base]);
2224
+ const generate = import_react2.default.useCallback(async (force = false) => {
2225
+ if (!name) return null;
2226
+ if (controller.current) return null;
2227
+ const ctrl = new AbortController();
2228
+ controller.current = ctrl;
2229
+ setGenerating(true);
2230
+ setGenError(null);
2231
+ try {
2232
+ const result = await apiCall(
2233
+ "POST",
2234
+ base,
2235
+ { lang: lang || "zh", force: Boolean(force) },
2236
+ { signal: ctrl.signal }
2237
+ );
2238
+ setState({ loading: false, intro: result.intro || null, capability: result.capability || null, error: null });
2239
+ return result;
2240
+ } catch (error) {
2241
+ if (error?.name === "AbortError") return null;
2242
+ setGenError({ code: error?.code || null, message: error?.message || "生成失败" });
2243
+ return null;
2244
+ } finally {
2245
+ if (controller.current === ctrl) controller.current = null;
2246
+ setGenerating(false);
2247
+ }
2248
+ }, [name, lang, base]);
2249
+ return { ...state, generating, genError, generate };
2250
+ }
2104
2251
 
2105
2252
  // client/views/header.js
2106
2253
  function installSpec(plugin) {
@@ -2294,18 +2441,164 @@ function Toolbar({
2294
2441
  }
2295
2442
 
2296
2443
  // client/views/installed.js
2297
- var import_react4 = __toESM(require("react"), 1);
2444
+ var import_react5 = __toESM(require("react"), 1);
2298
2445
 
2299
- // client/views/policy-popover.js
2446
+ // client/views/intro-popover.js
2300
2447
  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);
2448
+ var ERROR_KEY = {
2449
+ "error.skills.intro-no-model": "intro.error.noModel",
2450
+ "error.skills.intro-rate-limit": "intro.error.rateLimit",
2451
+ "error.skills.intro-quota": "intro.error.rateLimit",
2452
+ "error.skills.intro-too-large": "intro.error.tooLarge",
2453
+ "error.skills.intro-timeout": "intro.error.timeout",
2454
+ "error.skills.intro-auth": "intro.error.auth",
2455
+ "error.skills.intro-empty": "intro.error.empty",
2456
+ "error.skills.intro-busy": "intro.error.busy",
2457
+ "error.skills.intro-source": "intro.error.source",
2458
+ "error.skills.subscription-not-found": "intro.error.subscription",
2459
+ "error.skills.intro-source-timeout": "intro.error.sourceTimeout",
2460
+ "error.skills.intro-no-source": "intro.error.noSource"
2461
+ };
2462
+ function formatWhen(iso) {
2463
+ if (!iso) return null;
2464
+ const date = new Date(iso);
2465
+ if (Number.isNaN(date.getTime())) return null;
2466
+ const pad = (value) => String(value).padStart(2, "0");
2467
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
2468
+ }
2469
+ function modelLabel(intro) {
2470
+ if (!intro?.model) return "—";
2471
+ return intro.provider ? `${intro.provider}/${intro.model}` : intro.model;
2472
+ }
2473
+ function IntroPopover({
2474
+ t,
2475
+ name,
2476
+ stamp = "",
2477
+ subtitle = null,
2478
+ endpoint = null,
2479
+ capability,
2480
+ anchor,
2481
+ anchorEl,
2482
+ hint = null,
2483
+ viewLabel = null,
2484
+ onClose,
2485
+ onView = null
2486
+ }) {
2305
2487
  const ref = import_react3.default.useRef(null);
2306
2488
  const [pos, setPos] = import_react3.default.useState({ left: 0, top: 0, visibility: "hidden" });
2489
+ const enabled = capability?.enabled !== false;
2490
+ const available = capability?.available !== false;
2491
+ const intro = useSkillIntro({
2492
+ name,
2493
+ lang: t.lang || "zh",
2494
+ stamp,
2495
+ enabled,
2496
+ endpoint
2497
+ });
2307
2498
  useOverlayLayer(true, onClose);
2308
2499
  import_react3.default.useLayoutEffect(() => {
2500
+ const node = ref.current;
2501
+ if (!node || !anchor || typeof window === "undefined") return;
2502
+ const rect = node.getBoundingClientRect();
2503
+ const width = rect.width || 360;
2504
+ const height = rect.height || 220;
2505
+ let left = anchor.left;
2506
+ left = Math.max(8, Math.min(left, window.innerWidth - width - 8));
2507
+ let top = anchor.bottom + 8;
2508
+ if (top + height > window.innerHeight - 8) {
2509
+ top = Math.max(8, anchor.top - height - 8);
2510
+ }
2511
+ setPos({ left, top, visibility: "visible" });
2512
+ }, [anchor, intro.loading, intro.intro?.text, intro.generating, intro.genError]);
2513
+ import_react3.default.useEffect(() => {
2514
+ if (typeof document === "undefined") return void 0;
2515
+ const onPointerDown = (event) => {
2516
+ const target = event?.target;
2517
+ if (ref.current?.contains?.(target)) return;
2518
+ if (anchorEl?.contains?.(target)) return;
2519
+ onClose();
2520
+ };
2521
+ document.addEventListener("pointerdown", onPointerDown, true);
2522
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
2523
+ }, [anchorEl, onClose]);
2524
+ const failure = intro.genError || intro.error;
2525
+ const failureKey = failure?.code ? ERROR_KEY[failure.code] || "intro.error.generic" : null;
2526
+ const meta = intro.intro ? t("intro.meta").replace("{model}", modelLabel(intro.intro)).replace("{time}", formatWhen(intro.intro.generatedAt) || "—") : "";
2527
+ const actions = [];
2528
+ if (intro.intro) {
2529
+ actions.push(h("button", {
2530
+ type: "button",
2531
+ key: "regen",
2532
+ className: "dshsc-btn ghost",
2533
+ disabled: intro.generating || !available,
2534
+ onClick: () => {
2535
+ void intro.generate(true);
2536
+ }
2537
+ }, intro.generating ? t("intro.generating") : t("intro.regenerate")));
2538
+ if (onView) {
2539
+ actions.push(h("button", {
2540
+ type: "button",
2541
+ key: "view",
2542
+ className: "dshsc-btn primary",
2543
+ onClick: onView
2544
+ }, viewLabel || t("intro.view")));
2545
+ }
2546
+ } else {
2547
+ actions.push(h("button", {
2548
+ type: "button",
2549
+ key: "gen",
2550
+ className: "dshsc-btn primary",
2551
+ disabled: intro.generating || !available || !enabled,
2552
+ onClick: () => {
2553
+ void intro.generate(false);
2554
+ }
2555
+ }, intro.generating ? t("intro.generating") : t("intro.generate")));
2556
+ }
2557
+ return portalize(h(
2558
+ "div",
2559
+ {
2560
+ ref,
2561
+ className: "dshsc-policy-pop dshsc-intro-pop",
2562
+ role: "dialog",
2563
+ "aria-label": `${t("intro.title")} · ${name}`,
2564
+ style: { left: `${pos.left}px`, top: `${pos.top}px`, visibility: pos.visibility }
2565
+ },
2566
+ h(
2567
+ "div",
2568
+ { className: "dshsc-policy-head" },
2569
+ h(
2570
+ "div",
2571
+ null,
2572
+ h("div", { className: "dshsc-policy-title" }, `✨ ${t("intro.title")}`),
2573
+ h("div", { className: "dshsc-policy-sub", title: subtitle || name }, subtitle || name)
2574
+ ),
2575
+ h("button", {
2576
+ type: "button",
2577
+ className: "dshsc-policy-close",
2578
+ "aria-label": t("action.close"),
2579
+ onClick: onClose
2580
+ }, "×")
2581
+ ),
2582
+ 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")),
2583
+ intro.intro?.stale ? h("p", { className: "dshsc-intro-stale" }, t("intro.stale")) : null,
2584
+ failureKey ? h("p", { className: "dshsc-intro-error" }, t(failureKey)) : null,
2585
+ meta ? h("p", { className: "dshsc-intro-meta" }, meta) : null,
2586
+ available && !intro.intro && !intro.loading ? h("p", { className: "dshsc-intro-meta" }, hint || t("intro.hint")) : null,
2587
+ !available ? h("p", { className: "dshsc-intro-error" }, t("intro.unavailable")) : null,
2588
+ h("div", { className: "dshsc-intro-foot" }, actions)
2589
+ ));
2590
+ }
2591
+
2592
+ // client/views/policy-popover.js
2593
+ var import_react4 = __toESM(require("react"), 1);
2594
+ function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpark }) {
2595
+ const initial = row.mode === "hidden" ? "auto" : row.mode;
2596
+ const [mode, setMode] = import_react4.default.useState(initial);
2597
+ const [confirming, setConfirming] = import_react4.default.useState(false);
2598
+ const ref = import_react4.default.useRef(null);
2599
+ const [pos, setPos] = import_react4.default.useState({ left: 0, top: 0, visibility: "hidden" });
2600
+ useOverlayLayer(true, onClose);
2601
+ import_react4.default.useLayoutEffect(() => {
2309
2602
  const node = ref.current;
2310
2603
  if (!node || !anchor || typeof window === "undefined") return;
2311
2604
  const rect = node.getBoundingClientRect();
@@ -2319,7 +2612,7 @@ function PolicyPopover({ t, row, anchor, anchorEl, busy, onClose, onSave, onUnpa
2319
2612
  }
2320
2613
  setPos({ left, top, visibility: "visible" });
2321
2614
  }, [anchor, confirming, mode]);
2322
- import_react3.default.useEffect(() => {
2615
+ import_react4.default.useEffect(() => {
2323
2616
  if (typeof document === "undefined") return void 0;
2324
2617
  const onPointerDown = (event) => {
2325
2618
  const target = event?.target;
@@ -2473,13 +2766,28 @@ function InstalledTab({
2473
2766
  actions
2474
2767
  }) {
2475
2768
  const updatable = (row) => UPDATABLE.has(row.status);
2476
- const [policy, setPolicy] = import_react4.default.useState(null);
2769
+ const [policy, setPolicy] = import_react5.default.useState(null);
2770
+ const [intro, setIntro] = import_react5.default.useState(null);
2477
2771
  const sharedTargets = new Set((data?.agents?.detected || []).filter((agent) => agent.kind === "agent" && agent.sharedHome === "yes").map((agent) => agent.target));
2478
- import_react4.default.useEffect(() => {
2772
+ import_react5.default.useEffect(() => {
2479
2773
  if (!policy) return;
2480
2774
  if (rows.some((row) => row.name === policy.row.name)) return;
2481
2775
  setPolicy(null);
2482
2776
  }, [policy, rows]);
2777
+ import_react5.default.useEffect(() => {
2778
+ if (!intro) return;
2779
+ if (rows.some((row) => row.name === intro.row.name)) return;
2780
+ setIntro(null);
2781
+ }, [intro, rows]);
2782
+ function openIntro(event, row) {
2783
+ const el = event.currentTarget;
2784
+ const rect = el.getBoundingClientRect();
2785
+ setIntro({
2786
+ row,
2787
+ anchorEl: el,
2788
+ anchor: { left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }
2789
+ });
2790
+ }
2483
2791
  function openPolicy(event, row) {
2484
2792
  if (busy !== null || row.kind === "file") return;
2485
2793
  if (row.mode === "manual" && row.origin === "author") return;
@@ -2673,6 +2981,18 @@ function InstalledTab({
2673
2981
  "aria-haspopup": "dialog",
2674
2982
  onClick: () => actions.onView(row)
2675
2983
  }, row.name),
2984
+ data?.intro ? h(
2985
+ "button",
2986
+ {
2987
+ type: "button",
2988
+ className: "dshsc-intro-btn",
2989
+ title: t("intro.open"),
2990
+ "aria-haspopup": "dialog",
2991
+ onClick: (event) => openIntro(event, row)
2992
+ },
2993
+ h("span", { className: "dshsc-spark", "aria-hidden": "true" }, "✨"),
2994
+ t("intro.button")
2995
+ ) : null,
2676
2996
  statusTag(row)
2677
2997
  ),
2678
2998
  sourceLine(row),
@@ -2786,6 +3106,7 @@ function InstalledTab({
2786
3106
  }, dot, h("span", null, label), h("span", { className: "n" }, counts[id]));
2787
3107
  }
2788
3108
  function listBar() {
3109
+ const updateDot = counts.update > 0 ? h("i", { className: "dshsc-fdot" }) : null;
2789
3110
  return h(
2790
3111
  "div",
2791
3112
  { className: "dshsc-listbar" },
@@ -2793,7 +3114,7 @@ function InstalledTab({
2793
3114
  "div",
2794
3115
  { className: "dshsc-filters" },
2795
3116
  filterChip("all", t("filter.all")),
2796
- filterChip("update", t("filter.update"), h("i", { className: "dshsc-fdot" })),
3117
+ filterChip("update", t("filter.update"), updateDot),
2797
3118
  filterChip("unknown", t("filter.unknown")),
2798
3119
  filterChip("manual", t("filter.manual")),
2799
3120
  filterChip("off", t("filter.off"))
@@ -2921,6 +3242,20 @@ function InstalledTab({
2921
3242
  )
2922
3243
  );
2923
3244
  }
3245
+ const introPopover = intro && data?.intro ? h(IntroPopover, {
3246
+ t,
3247
+ name: intro.row.name,
3248
+ stamp: [intro.row.commit || "", intro.row.currentHash || "", intro.row.updatedAt || "", intro.row.status || ""].join("|"),
3249
+ anchor: intro.anchor,
3250
+ anchorEl: intro.anchorEl,
3251
+ capability: data.intro,
3252
+ onClose: () => setIntro(null),
3253
+ onView: () => {
3254
+ const target = intro.row;
3255
+ setIntro(null);
3256
+ actions.onView(target);
3257
+ }
3258
+ }) : null;
2924
3259
  const popover = policy ? h(PolicyPopover, {
2925
3260
  t,
2926
3261
  row: policy.row,
@@ -2939,7 +3274,7 @@ function InstalledTab({
2939
3274
  actions.onRequestUnpark(target);
2940
3275
  }
2941
3276
  }) : null;
2942
- return h(import_react4.default.Fragment, null, content(), popover);
3277
+ return h(import_react5.default.Fragment, null, content(), popover, introPopover);
2943
3278
  }
2944
3279
 
2945
3280
  // client/views/agent-tip.js
@@ -2974,10 +3309,14 @@ function AgentTip({ t, agentTip, agentTipPos, nodeRef }) {
2974
3309
  }
2975
3310
 
2976
3311
  // client/views/subscriptions.js
3312
+ var import_react6 = __toESM(require("react"), 1);
2977
3313
  function repoOf(source) {
2978
3314
  const value = String(source || "").trim().toLowerCase().replace(/\.git$/, "");
2979
3315
  return value || null;
2980
3316
  }
3317
+ function shortCommit(value) {
3318
+ return typeof value === "string" ? value.slice(0, 7) : "";
3319
+ }
2981
3320
  function SubscriptionsTab({
2982
3321
  t,
2983
3322
  subscriptions,
@@ -2985,12 +3324,72 @@ function SubscriptionsTab({
2985
3324
  expanded,
2986
3325
  busy,
2987
3326
  writable,
3327
+ capability = null,
2988
3328
  onToggle,
2989
3329
  onRefresh,
2990
3330
  onDelete,
2991
3331
  onInstall,
2992
3332
  onAdd
2993
3333
  }) {
3334
+ const [intro, setIntro] = import_react6.default.useState(null);
3335
+ import_react6.default.useEffect(() => {
3336
+ if (!intro) return;
3337
+ const subscription = subscriptions.find((item) => item.id === intro.subscriptionId);
3338
+ const entries = subscription?.snapshot?.entries || [];
3339
+ if (entries.some((entry) => entry.name === intro.name)) return;
3340
+ setIntro(null);
3341
+ }, [intro, subscriptions]);
3342
+ function openIntro(event, subscription, entry) {
3343
+ const el = event.currentTarget;
3344
+ const rect = el.getBoundingClientRect();
3345
+ setIntro({
3346
+ subscriptionId: subscription.id,
3347
+ name: entry.name,
3348
+ anchorEl: el,
3349
+ anchor: { left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }
3350
+ });
3351
+ }
3352
+ function introButton(subscription, entry) {
3353
+ return h(
3354
+ "button",
3355
+ {
3356
+ type: "button",
3357
+ className: "dshsc-intro-btn",
3358
+ title: t("intro.open"),
3359
+ "aria-haspopup": "dialog",
3360
+ onClick: (clickEvent) => openIntro(clickEvent, subscription, entry)
3361
+ },
3362
+ h("span", { className: "dshsc-spark", "aria-hidden": "true" }, "✨"),
3363
+ t("intro.button")
3364
+ );
3365
+ }
3366
+ const introSubscription = intro ? subscriptions.find((item) => item.id === intro.subscriptionId) || null : null;
3367
+ const introEntry = introSubscription ? (introSubscription.snapshot?.entries || []).find((entry) => entry.name === intro.name) || null : null;
3368
+ const introPopover = intro && introEntry && capability ? (() => {
3369
+ const commit = introSubscription.snapshot?.commit || "";
3370
+ const installedAlready = rows.some((row) => {
3371
+ if (row.name !== introEntry.name) return false;
3372
+ if (row.subscriptionId === introSubscription.id) return true;
3373
+ return repoOf(row.source) === repoOf(introSubscription.source);
3374
+ });
3375
+ return h(IntroPopover, {
3376
+ t,
3377
+ name: introEntry.name,
3378
+ stamp: commit,
3379
+ subtitle: `${introSubscription.source}@${shortCommit(commit)}`,
3380
+ endpoint: `/subscriptions/${encodeURIComponent(introSubscription.id)}/entries/${encodeURIComponent(introEntry.name)}/intro`,
3381
+ capability,
3382
+ anchor: intro.anchor,
3383
+ anchorEl: intro.anchorEl,
3384
+ hint: t("intro.hintRemote").replace("{source}", introSubscription.source).replace("{commit}", shortCommit(commit) || "—"),
3385
+ viewLabel: t("action.install"),
3386
+ onView: installedAlready ? null : () => {
3387
+ setIntro(null);
3388
+ void onInstall(introSubscription, introEntry);
3389
+ },
3390
+ onClose: () => setIntro(null)
3391
+ });
3392
+ })() : null;
2994
3393
  if (subscriptions.length === 0) {
2995
3394
  return h(
2996
3395
  "div",
@@ -3013,118 +3412,133 @@ function SubscriptionsTab({
3013
3412
  );
3014
3413
  }
3015
3414
  return h(
3016
- "div",
3017
- { className: "dshsc-tabpane" },
3018
- // 表头与仓库行共用同一个滚动容器,位置 sticky;宽屏 4 列,窄屏收成「仓库」/「操作」。
3415
+ import_react6.default.Fragment,
3416
+ null,
3019
3417
  h(
3020
3418
  "div",
3021
- { className: "dshsc-listscroll dshsc-subscroll" },
3419
+ { className: "dshsc-tabpane" },
3420
+ // 表头与仓库行共用同一个滚动容器,位置 sticky;宽屏 4 列,窄屏收成「仓库」/「操作」。
3022
3421
  h(
3023
3422
  "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(
3423
+ { className: "dshsc-listscroll dshsc-subscroll" },
3424
+ h(
3043
3425
  "div",
3044
- { className: "dshsc-sub-block", key: subscription.id },
3045
- h(
3426
+ { className: "dshsc-subhead" },
3427
+ h("div", { className: "dshsc-hrepo" }, t("column.repo")),
3428
+ h("div", { className: "dshsc-href" }, t("column.ref")),
3429
+ h("div", { className: "dshsc-hcount" }, t("column.entries")),
3430
+ h("div", { className: "dshsc-hact" }, t("column.actions"))
3431
+ ),
3432
+ subscriptions.map((subscription) => {
3433
+ const entries = subscription.snapshot?.entries || [];
3434
+ const repo = repoOf(subscription.source);
3435
+ const rowsByName = new Map(rows.map((row) => [row.name, row]));
3436
+ const installedRow = (name) => {
3437
+ const row = rowsByName.get(name);
3438
+ if (!row) return null;
3439
+ if (row.subscriptionId === subscription.id) return row;
3440
+ return repo && repoOf(row.source) === repo ? row : null;
3441
+ };
3442
+ const installedCount = entries.filter((entry) => installedRow(entry.name)).length;
3443
+ const open = Boolean(expanded[subscription.id]);
3444
+ return h(
3046
3445
  "div",
3047
- { className: "dshsc-sub-row" },
3446
+ { className: "dshsc-sub-block", key: subscription.id },
3048
3447
  h(
3049
3448
  "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 / 条目两列;窄屏回到仓库名下方一行。
3060
- h(
3061
- "div",
3062
- { className: "dshsc-sub-meta" },
3063
- h("span", { className: "dshsc-tag dshsc-subref" }, subscription.ref || t("subscription.defaultRef")),
3449
+ { className: "dshsc-sub-row" },
3450
+ h(
3451
+ "div",
3452
+ { className: "dshsc-sub-repo" },
3453
+ h("button", {
3454
+ className: "dshsc-caret",
3455
+ "aria-expanded": String(open),
3456
+ "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
3457
+ onClick: () => onToggle(subscription.id)
3458
+ }, h(CaretIcon, { open })),
3459
+ h("span", { className: "dshsc-name" }, subscription.source)
3460
+ ),
3461
+ // 宽屏铺成 Ref / 条目两列;窄屏回到仓库名下方一行。
3064
3462
  h(
3065
- "span",
3066
- { className: "dshsc-substat" },
3067
- `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedCount}`
3463
+ "div",
3464
+ { className: "dshsc-sub-meta" },
3465
+ h("span", { className: "dshsc-tag dshsc-subref" }, subscription.ref || t("subscription.defaultRef")),
3466
+ h(
3467
+ "span",
3468
+ { className: "dshsc-substat" },
3469
+ `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedCount}`
3470
+ )
3471
+ ),
3472
+ h(
3473
+ "div",
3474
+ { className: "dshsc-actions" },
3475
+ h("button", {
3476
+ className: "dshsc-btn sq",
3477
+ disabled: busy !== null,
3478
+ title: t("action.checkUpdates"),
3479
+ "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
3480
+ onClick: () => void onRefresh(subscription)
3481
+ }, h(RefreshIcon, { size: 13 })),
3482
+ h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
3068
3483
  )
3069
3484
  ),
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(
3485
+ open ? h(
3087
3486
  "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(
3487
+ { className: "dshsc-sub-list" },
3488
+ h(
3096
3489
  "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(
3490
+ { className: "dshsc-entryhead" },
3491
+ h("div", null, t("column.skill")),
3492
+ h("div", null, t("column.status")),
3493
+ h("div", { className: "right" }, t("column.actions"))
3494
+ ),
3495
+ entries.map((entry) => {
3496
+ const installedAlready = Boolean(installedRow(entry.name));
3497
+ return h(
3106
3498
  "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
- )
3499
+ { className: "dshsc-sub-entry", key: entry.name },
3500
+ h(
3501
+ "div",
3502
+ { className: "dshsc-sub-skill" },
3503
+ h(
3504
+ "div",
3505
+ { className: "dshsc-sub-namerow" },
3506
+ h("span", { className: "dshsc-name" }, entry.name),
3507
+ capability ? introButton(subscription, entry) : null
3508
+ ),
3509
+ entry.description ? h("div", { className: "dshsc-sub-desc", title: entry.description }, entry.description) : null
3510
+ ),
3511
+ h(
3512
+ "span",
3513
+ { className: `dshsc-state${installedAlready ? " ok" : ""}` },
3514
+ installedAlready ? "✓ " : "",
3515
+ installedAlready ? t("installed") : t("notInstalled")
3516
+ ),
3517
+ h(
3518
+ "div",
3519
+ { className: "dshsc-actions" },
3520
+ installedAlready ? h("span", { className: "dshsc-noop" }, "-") : h("button", {
3521
+ className: "dshsc-btn",
3522
+ disabled: !writable || busy !== null,
3523
+ onClick: () => void onInstall(subscription, entry)
3524
+ }, t("action.install"))
3525
+ )
3526
+ );
3527
+ })
3528
+ ) : null
3529
+ );
3530
+ })
3531
+ )
3532
+ ),
3533
+ introPopover
3120
3534
  );
3121
3535
  }
3122
3536
 
3123
3537
  // client/views/modals.js
3124
- var import_react6 = __toESM(require("react"), 1);
3538
+ var import_react8 = __toESM(require("react"), 1);
3125
3539
 
3126
3540
  // client/views/add-skill.js
3127
- var import_react5 = __toESM(require("react"), 1);
3541
+ var import_react7 = __toESM(require("react"), 1);
3128
3542
 
3129
3543
  // client/views/overlay.js
3130
3544
  function Overlay({ t, title, children, footer, onCancel }) {
@@ -3154,13 +3568,13 @@ function Overlay({ t, title, children, footer, onCancel }) {
3154
3568
 
3155
3569
  // client/views/add-skill.js
3156
3570
  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);
3571
+ const [source, setSource] = import_react7.default.useState("");
3572
+ const [phase, setPhase] = import_react7.default.useState("idle");
3573
+ const [options, setOptions] = import_react7.default.useState([]);
3574
+ const [selected, setSelected] = import_react7.default.useState(() => /* @__PURE__ */ new Set());
3575
+ const [manual, setManual] = import_react7.default.useState("");
3576
+ const [skipped, setSkipped] = import_react7.default.useState(0);
3577
+ const [error, setError] = import_react7.default.useState(null);
3164
3578
  const reset = (value) => {
3165
3579
  setSource(value);
3166
3580
  setPhase("idle");
@@ -3296,7 +3710,7 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3296
3710
 
3297
3711
  // client/views/modals.js
3298
3712
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
3299
- const [form, setForm] = import_react6.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
3713
+ const [form, setForm] = import_react8.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
3300
3714
  const incomplete = fields.some(
3301
3715
  (field) => field.required !== false && !String(form[field.key] || "").trim()
3302
3716
  );
@@ -3344,7 +3758,7 @@ function ExportsModal({ t, row, agents, onCancel, onSave }) {
3344
3758
  const candidates = agents.filter(
3345
3759
  (agent) => agent.kind === "agent" && agent.exists && agent.exportable
3346
3760
  );
3347
- const [targets, setTargets] = import_react6.default.useState(() => {
3761
+ const [targets, setTargets] = import_react8.default.useState(() => {
3348
3762
  const available = new Set(candidates.map((agent) => agent.target));
3349
3763
  return new Set((row.exports || []).map((item) => item.target).filter((target) => available.has(target)));
3350
3764
  });
@@ -3386,7 +3800,7 @@ function ExportsModal({ t, row, agents, onCancel, onSave }) {
3386
3800
  );
3387
3801
  }
3388
3802
  function DefaultExportModal({ t, settings, agents, onCancel, onSave }) {
3389
- const [targets, setTargets] = import_react6.default.useState(() => new Set(
3803
+ const [targets, setTargets] = import_react8.default.useState(() => new Set(
3390
3804
  Array.isArray(settings.exportTargets) ? settings.exportTargets : []
3391
3805
  ));
3392
3806
  const toggle = (target) => setTargets((prev) => {
@@ -3515,7 +3929,7 @@ ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
3515
3929
  }
3516
3930
 
3517
3931
  // client/views/skill-reader.js
3518
- var import_react7 = __toESM(require("react"), 1);
3932
+ var import_react9 = __toESM(require("react"), 1);
3519
3933
 
3520
3934
  // client/markdown.js
3521
3935
  function classifyHref(href) {
@@ -3951,7 +4365,7 @@ function formatBytes(bytes) {
3951
4365
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3952
4366
  return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3953
4367
  }
3954
- function formatWhen(iso) {
4368
+ function formatWhen2(iso) {
3955
4369
  if (!iso) return null;
3956
4370
  const date = new Date(iso);
3957
4371
  if (Number.isNaN(date.getTime())) return null;
@@ -4011,37 +4425,37 @@ function SkillReader({
4011
4425
  onToast
4012
4426
  }) {
4013
4427
  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
4428
  const name = row?.name || "";
4429
+ const [tab, setTab] = import_react9.default.useState(initialTab);
4430
+ const [format, setFormat] = import_react9.default.useState("rendered");
4431
+ const [docOverride, setDocOverride] = import_react9.default.useState(null);
4432
+ const [fileFormat, setFileFormat] = import_react9.default.useState("rendered");
4433
+ const [tocActive, setTocActive] = import_react9.default.useState(null);
4434
+ const [descOpen, setDescOpen] = import_react9.default.useState(false);
4435
+ const [descClamped, setDescClamped] = import_react9.default.useState(false);
4436
+ const bodyRef = import_react9.default.useRef(null);
4437
+ const dialogRef = import_react9.default.useRef(null);
4438
+ const descRef = import_react9.default.useRef(null);
4439
+ const scrollRef = import_react9.default.useRef(0);
4440
+ useOverlayLayer(variant === "drawer" || variant === "page", onClose);
4027
4441
  const tree = state.tree;
4028
4442
  const files = tree?.files || [];
4029
4443
  const fileCount = files.filter((entry) => entry.kind === "file").length;
4030
4444
  const bodyText = docOverride ? docOverride.content : state.body?.content || "";
4031
4445
  const description = row?.description || state.body?.meta?.description || "";
4032
4446
  const headings = tab === "doc" && bodyText ? parseCached(bodyText).headings : [];
4033
- import_react7.default.useEffect(() => {
4447
+ import_react9.default.useEffect(() => {
4034
4448
  setDocOverride(null);
4035
4449
  scrollRef.current = 0;
4036
4450
  }, [name]);
4037
- const descOwnerRef = import_react7.default.useRef(name);
4038
- import_react7.default.useEffect(() => {
4451
+ const descOwnerRef = import_react9.default.useRef(name);
4452
+ import_react9.default.useEffect(() => {
4039
4453
  if (descOwnerRef.current === name) return;
4040
4454
  descOwnerRef.current = name;
4041
4455
  setDescOpen(false);
4042
4456
  setDescClamped(false);
4043
4457
  }, [name]);
4044
- import_react7.default.useLayoutEffect(() => {
4458
+ import_react9.default.useLayoutEffect(() => {
4045
4459
  const node = descRef.current;
4046
4460
  if (!node) return void 0;
4047
4461
  const measure = () => {
@@ -4063,16 +4477,16 @@ function SkillReader({
4063
4477
  window.removeEventListener("resize", measure);
4064
4478
  };
4065
4479
  }, [description, descOpen]);
4066
- import_react7.default.useEffect(() => {
4480
+ import_react9.default.useEffect(() => {
4067
4481
  if (!initialFile) return;
4068
4482
  setTab("files");
4069
4483
  void loadFile(initialFile);
4070
4484
  }, [initialFile, loadFile]);
4071
- import_react7.default.useEffect(() => {
4485
+ import_react9.default.useEffect(() => {
4072
4486
  const node = bodyRef.current;
4073
4487
  if (node && scrollRef.current) node.scrollTop = scrollRef.current;
4074
4488
  });
4075
- import_react7.default.useEffect(() => {
4489
+ import_react9.default.useEffect(() => {
4076
4490
  if (variant !== "drawer" || typeof document === "undefined") return void 0;
4077
4491
  const previous = document.activeElement;
4078
4492
  const node = dialogRef.current;
@@ -4099,12 +4513,12 @@ function SkillReader({
4099
4513
  previous?.focus?.();
4100
4514
  };
4101
4515
  }, [variant, onClose]);
4102
- import_react7.default.useEffect(() => {
4516
+ import_react9.default.useEffect(() => {
4103
4517
  if (variant !== "page" || typeof document === "undefined") return;
4104
4518
  const node = document.getElementById("dshsc-reader-name");
4105
4519
  node?.focus?.();
4106
4520
  }, [variant, name]);
4107
- import_react7.default.useEffect(() => {
4521
+ import_react9.default.useEffect(() => {
4108
4522
  if (variant !== "page" || typeof window === "undefined") return void 0;
4109
4523
  const node = bodyRef.current;
4110
4524
  if (!node?.querySelectorAll) return void 0;
@@ -4200,7 +4614,7 @@ function SkillReader({
4200
4614
  h("span", { className: "dshsc-code" }, row?.source ? `${row.source}${row.commit ? `#${short(row.commit)}` : row.ref ? `#${row.ref}` : ""}` : t("meta.noUpstream"))
4201
4615
  ),
4202
4616
  tree ? h("span", null, t("viewer.fileCount").replace("{n}", String(fileCount))) : null,
4203
- row?.updatedAt ? h("span", null, formatWhen(row.updatedAt)) : null
4617
+ row?.updatedAt ? h("span", null, formatWhen2(row.updatedAt)) : null
4204
4618
  )
4205
4619
  );
4206
4620
  const tabsBar = h(
@@ -4417,8 +4831,8 @@ function SkillReader({
4417
4831
  ["viewer.info.hash", hash ? h("span", { className: "mono" }, hash) : t("viewer.info.none"), "mono"]
4418
4832
  ];
4419
4833
  const localRows = [
4420
- ["viewer.info.installedAt", formatWhen(row?.installedAt) || t("viewer.info.none")],
4421
- ["viewer.info.updatedAt", formatWhen(row?.updatedAt) || t("viewer.info.none")],
4834
+ ["viewer.info.installedAt", formatWhen2(row?.installedAt) || t("viewer.info.none")],
4835
+ ["viewer.info.updatedAt", formatWhen2(row?.updatedAt) || t("viewer.info.none")],
4422
4836
  ["viewer.info.size", tree ? `${formatBytes(tree.totalBytes)} · ${t("viewer.fileCount").replace("{n}", String(fileCount))}` : t("viewer.info.none")],
4423
4837
  ["viewer.info.localChange", row?.status === "locally-modified" || row?.status === "conflict" ? t("viewer.info.localChangeYes") : t("viewer.info.localChangeNone")]
4424
4838
  ];
@@ -4542,19 +4956,19 @@ function SkillReader({
4542
4956
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
4543
4957
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4544
4958
  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);
4959
+ const [tab, setTab] = import_react10.default.useState("installed");
4960
+ const [view, setView] = import_react10.default.useState("flat");
4961
+ const [modal, setModal] = import_react10.default.useState(null);
4962
+ const [expanded, setExpanded] = import_react10.default.useState({});
4963
+ const [placementBusy, setPlacementBusy] = import_react10.default.useState(null);
4964
+ const [placementOverride, setPlacementOverride] = import_react10.default.useState(null);
4965
+ const [query, setQuery] = import_react10.default.useState("");
4966
+ const [viewer, setViewer] = import_react10.default.useState(null);
4967
+ const rootRef = import_react10.default.useRef(null);
4968
+ const scrollPos = import_react10.default.useRef(null);
4969
+ const restoreScroll = import_react10.default.useRef(null);
4970
+ const [filter, setFilter] = import_react10.default.useState("all");
4971
+ const [builtinOpen, setBuiltinOpen] = import_react10.default.useState(false);
4558
4972
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
4559
4973
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
4560
4974
  const rows = data?.installs || [];
@@ -4577,7 +4991,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4577
4991
  const agentLabels = row.exports.map((item) => item.label);
4578
4992
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
4579
4993
  });
4580
- import_react8.default.useEffect(() => {
4994
+ import_react10.default.useEffect(() => {
4581
4995
  if (!viewer || !data) return;
4582
4996
  if (rows.some((row) => row.name === viewer.name)) return;
4583
4997
  setViewer(null);
@@ -4611,7 +5025,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4611
5025
  if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
4612
5026
  setViewer(null);
4613
5027
  }
4614
- import_react8.default.useLayoutEffect(() => {
5028
+ import_react10.default.useLayoutEffect(() => {
4615
5029
  if (restoreScroll.current === null) return;
4616
5030
  const target = restoreScroll.current;
4617
5031
  restoreScroll.current = null;
@@ -4680,7 +5094,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4680
5094
  onToast: (next) => setToast(next)
4681
5095
  }) : null;
4682
5096
  const listNode = h(
4683
- import_react8.default.Fragment,
5097
+ import_react10.default.Fragment,
4684
5098
  null,
4685
5099
  h(Header, {
4686
5100
  t,
@@ -4738,6 +5152,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4738
5152
  expanded,
4739
5153
  busy,
4740
5154
  writable,
5155
+ capability: data?.intro || null,
4741
5156
  ...subscriptionActions
4742
5157
  }),
4743
5158
  busy ? h("div", { className: "dshsc-meta" }, `${t("busy.working")} ${busy}`) : null