@morehao/dsh-skills-console 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/client.js +304 -243
  2. package/package.json +3 -2
package/client.js CHANGED
@@ -848,7 +848,26 @@ function FolderIcon({ size = 12 }) {
848
848
  }
849
849
 
850
850
  // client/section.js
851
- var import_react5 = __toESM(require("react"), 1);
851
+ var import_react6 = __toESM(require("react"), 1);
852
+
853
+ // client/scroll.js
854
+ function isScroller(node, tolerance = 4) {
855
+ if (!node || typeof getComputedStyle !== "function") return false;
856
+ const style = getComputedStyle(node);
857
+ return Boolean(style && /(auto|scroll)/.test(style.overflowY || "") && node.scrollHeight > node.clientHeight + tolerance);
858
+ }
859
+ function nearestScroller(node, tolerance = 4) {
860
+ let current = node?.parentElement || null;
861
+ while (current) {
862
+ if (isScroller(current, tolerance)) return current;
863
+ current = current.parentElement || null;
864
+ }
865
+ return typeof document !== "undefined" ? document.scrollingElement || null : null;
866
+ }
867
+ function scrollHost(node, tolerance = 4) {
868
+ if (isScroller(node, tolerance)) return node;
869
+ return node && nearestScroller(node, tolerance) || node;
870
+ }
852
871
 
853
872
  // client/styles.js
854
873
  var CSS = `
@@ -1267,8 +1286,9 @@ var CSS = `
1267
1286
  color: var(--dsw-alias-label-secondary); line-height: 1.6; }
1268
1287
  .dshsc-reader-placeholder b { color: var(--dsw-alias-label-primary); font-size: 13px; }
1269
1288
 
1270
- /* 文件 tab:左树右预览;抽屉里上下排 */
1271
- .dshsc-reader-files { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
1289
+ /* 文件 tab:抽屉里上下排;整页(宽容器)左树右预览 */
1290
+ .dshsc-reader-files { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
1291
+ .dshsc-reader-page .dshsc-reader-files { grid-template-columns: minmax(200px, 240px) minmax(0, 1fr); }
1272
1292
  .dshsc-reader-tree { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: auto;
1273
1293
  max-height: 300px; background: var(--dsw-alias-bg-layer-1); }
1274
1294
  .dshsc-reader-preview { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: hidden;
@@ -1386,8 +1406,11 @@ var CSS = `
1386
1406
  background: var(--dsw-alias-bg-layer-1); }
1387
1407
  .dshsc-reader-hero .dshsc-reader-head { border-bottom: 0; }
1388
1408
  .dshsc-reader-hero .dshsc-reader-desc { -webkit-line-clamp: 3; max-width: 78ch; }
1389
- .dshsc-reader-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 180px minmax(0, 1fr);
1409
+ .dshsc-reader-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr);
1390
1410
  gap: 18px; align-items: stretch; }
1411
+ /* 只有目录真的在的时候才开第二轨:否则唯一的正文列会被自动摆进 180px 的目录轨,
1412
+ 文件 / 信息 tab 会窄成一条(树里的文件名被截断、预览挤到下面)。 */
1413
+ .dshsc-reader-layout.has-toc { grid-template-columns: 180px minmax(0, 1fr); }
1391
1414
  .dshsc-reader-toc { position: sticky; top: 0; display: flex; flex-direction: column; gap: 1px;
1392
1415
  max-height: 70vh; overflow: auto; padding-right: 4px; }
1393
1416
  .dshsc-reader-toc-title { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase;
@@ -1413,11 +1436,20 @@ var CSS = `
1413
1436
  @container dshsc (max-width: 860px) {
1414
1437
  /* 行内「查看」先让位:技能名本身仍是入口 */
1415
1438
  .dshsc-row .dshsc-view { display: none; }
1416
- .dshsc-reader-layout { grid-template-columns: minmax(0, 1fr); }
1439
+ .dshsc-reader-layout.has-toc { grid-template-columns: minmax(0, 1fr); }
1440
+ .dshsc-reader-page .dshsc-reader-files { grid-template-columns: minmax(0, 1fr); }
1417
1441
  .dshsc-reader-toc { display: none; }
1418
1442
  .dshsc-reader-page .dshsc-reader-desc { -webkit-line-clamp: 2; }
1419
1443
  }
1420
1444
 
1445
+ /* 描述默认按行数截断;真被截断时下面给「展开 / 收起」,所以要压在上面所有 clamp 规则之后 */
1446
+ .dshsc-reader-descblock { display: flex; flex-direction: column; align-items: flex-start; }
1447
+ .dshsc-reader-desc.expanded { display: block; overflow: visible; -webkit-line-clamp: unset; }
1448
+ .dshsc-reader-more { margin-top: 3px; padding: 0; border: 0; border-radius: 4px; background: transparent;
1449
+ color: var(--dsw-alias-brand-primary); font: inherit; font-size: 11.5px; line-height: 1.6; cursor: pointer; }
1450
+ .dshsc-reader-more:hover { text-decoration: underline; }
1451
+ .dshsc-reader-more:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1452
+
1421
1453
  .dshsc-toast { position: fixed; right: 18px; bottom: 18px; z-index: 70; font-size: 12px; padding: 10px 13px; border-radius: 9px;
1422
1454
  background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2);
1423
1455
  box-shadow: 0 8px 24px rgba(0,0,0,.22); max-width: 420px; }
@@ -2447,7 +2479,12 @@ function SubscriptionsTab({
2447
2479
  }
2448
2480
 
2449
2481
  // client/views/modals.js
2482
+ var import_react4 = __toESM(require("react"), 1);
2483
+
2484
+ // client/views/add-skill.js
2450
2485
  var import_react3 = __toESM(require("react"), 1);
2486
+
2487
+ // client/views/overlay.js
2451
2488
  function Overlay({ t, title, children, footer, onCancel }) {
2452
2489
  useOverlayLayer(true, onCancel);
2453
2490
  return portalize(h(
@@ -2472,8 +2509,152 @@ function Overlay({ t, title, children, footer, onCancel }) {
2472
2509
  )
2473
2510
  ), overlayHost());
2474
2511
  }
2512
+
2513
+ // client/views/add-skill.js
2514
+ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
2515
+ const [source, setSource] = import_react3.default.useState("");
2516
+ const [phase, setPhase] = import_react3.default.useState("idle");
2517
+ const [options, setOptions] = import_react3.default.useState([]);
2518
+ const [selected, setSelected] = import_react3.default.useState(() => /* @__PURE__ */ new Set());
2519
+ const [manual, setManual] = import_react3.default.useState("");
2520
+ const [skipped, setSkipped] = import_react3.default.useState(0);
2521
+ const [error, setError] = import_react3.default.useState(null);
2522
+ const reset = (value) => {
2523
+ setSource(value);
2524
+ setPhase("idle");
2525
+ setOptions([]);
2526
+ setSelected(/* @__PURE__ */ new Set());
2527
+ setSkipped(0);
2528
+ setError(null);
2529
+ };
2530
+ const probe = async () => {
2531
+ const value = source.trim();
2532
+ if (!value || phase === "probing") return;
2533
+ setPhase("probing");
2534
+ setError(null);
2535
+ try {
2536
+ const result = await onProbe(value);
2537
+ const found = Array.isArray(result?.skills) ? result.skills : [];
2538
+ setOptions(found);
2539
+ setSelected(new Set(found.map((item) => item.name)));
2540
+ setSkipped(Math.max(0, Number(result?.skipped) || 0));
2541
+ setPhase(found.length > 0 ? "picked" : "empty");
2542
+ } catch (err) {
2543
+ setError(err?.message || String(err));
2544
+ setPhase("failed");
2545
+ }
2546
+ };
2547
+ const toggle = (name) => setSelected((prev) => {
2548
+ const next = new Set(prev);
2549
+ if (next.has(name)) next.delete(name);
2550
+ else next.add(name);
2551
+ return next;
2552
+ });
2553
+ const names = manual.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
2554
+ const skippedText = skipped > 0 ? t("form.skipped").replace("{n}", String(skipped)) : null;
2555
+ const manualMode = phase === "empty" || phase === "failed";
2556
+ const skills = phase === "picked" ? [...selected] : names;
2557
+ const canSubmit = phase === "picked" ? selected.size > 0 : manualMode ? names.length > 0 : false;
2558
+ const submit = () => {
2559
+ if (!canSubmit) return;
2560
+ onInstall({ source: source.trim(), skills });
2561
+ };
2562
+ return h(
2563
+ Overlay,
2564
+ {
2565
+ t,
2566
+ title: t("action.addSkill"),
2567
+ onCancel,
2568
+ footer: [
2569
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2570
+ h("button", {
2571
+ key: "s",
2572
+ className: "dshsc-btn primary",
2573
+ disabled: !canSubmit,
2574
+ onClick: submit
2575
+ }, t("action.save"))
2576
+ ]
2577
+ },
2578
+ h("p", { className: "dshsc-sub" }, t("form.pickHint")),
2579
+ h(
2580
+ "div",
2581
+ { className: "dshsc-field" },
2582
+ h("span", null, t("form.source")),
2583
+ h(
2584
+ "div",
2585
+ { className: "dshsc-inline" },
2586
+ h("input", {
2587
+ value: source,
2588
+ placeholder: t("form.sourceHint"),
2589
+ onChange: (event) => reset(event.target.value),
2590
+ onKeyDown: (event) => {
2591
+ if (event.key === "Enter") void probe();
2592
+ }
2593
+ }),
2594
+ h("button", {
2595
+ className: "dshsc-btn",
2596
+ disabled: !source.trim() || phase === "probing",
2597
+ onClick: () => void probe()
2598
+ }, phase === "probing" ? t("form.probing") : t("form.probe"))
2599
+ )
2600
+ ),
2601
+ phase === "picked" ? h(
2602
+ "div",
2603
+ { key: "picked" },
2604
+ h("div", { className: "dshsc-group" }, t("form.detected").replace("{n}", String(options.length))),
2605
+ h(
2606
+ "div",
2607
+ { style: { display: "flex", gap: 6, marginBottom: 6 } },
2608
+ h("button", {
2609
+ className: "dshsc-btn ghost",
2610
+ onClick: () => setSelected(new Set(options.map((item) => item.name)))
2611
+ }, t("form.selectAll")),
2612
+ h("button", {
2613
+ className: "dshsc-btn ghost",
2614
+ onClick: () => setSelected(/* @__PURE__ */ new Set())
2615
+ }, t("form.selectNone"))
2616
+ ),
2617
+ h("div", { className: "dshsc-panel" }, options.map((option) => h(
2618
+ "label",
2619
+ { className: "dshsc-check dshsc-skill", key: option.name },
2620
+ h("input", {
2621
+ type: "checkbox",
2622
+ checked: selected.has(option.name),
2623
+ onChange: () => toggle(option.name)
2624
+ }),
2625
+ h(
2626
+ "span",
2627
+ { className: "dshsc-skill-main" },
2628
+ h("span", { className: "dshsc-skill-name" }, option.name),
2629
+ option.description ? h("small", { className: "dshsc-skill-desc" }, option.description) : null,
2630
+ h("small", { className: "dshsc-skill-path" }, option.skillPath)
2631
+ )
2632
+ ))),
2633
+ skippedText ? h("p", { className: "dshsc-sub" }, skippedText) : null
2634
+ ) : null,
2635
+ phase === "probing" ? h("p", { className: "dshsc-sub" }, t("form.probingHint")) : null,
2636
+ manualMode ? h(
2637
+ "div",
2638
+ { key: "manual" },
2639
+ h("p", { className: "dshsc-sub" }, phase === "empty" ? skippedText || t("form.noSkills") : `${t("form.probeFailed")}${error ? ` · ${error}` : ""}`),
2640
+ h(
2641
+ "label",
2642
+ { className: "dshsc-field" },
2643
+ h("span", null, t("form.skills")),
2644
+ h("input", {
2645
+ value: manual,
2646
+ placeholder: "git-kit, handoff",
2647
+ onChange: (event) => setManual(event.target.value)
2648
+ })
2649
+ )
2650
+ ) : null,
2651
+ h("p", { className: "dshsc-sub" }, t("form.hint"))
2652
+ );
2653
+ }
2654
+
2655
+ // client/views/modals.js
2475
2656
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
2476
- const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
2657
+ const [form, setForm] = import_react4.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
2477
2658
  const incomplete = fields.some(
2478
2659
  (field) => field.required !== false && !String(form[field.key] || "").trim()
2479
2660
  );
@@ -2521,14 +2702,14 @@ function LinksModal({ t, row, agents, workspaces, pickDirectory, onCancel, onSav
2521
2702
  const candidates = agents.filter(
2522
2703
  (agent) => agent.kind === "dsh" || agent.kind !== "project" && agent.exists
2523
2704
  );
2524
- const [targets, setTargets] = import_react3.default.useState(() => {
2705
+ const [targets, setTargets] = import_react4.default.useState(() => {
2525
2706
  const visible = new Set(candidates.map((agent) => agent.target));
2526
2707
  const initial = new Set(row.links.map((link) => link.target).filter((target) => visible.has(target)));
2527
2708
  initial.add("dsh-global");
2528
2709
  return initial;
2529
2710
  });
2530
- const [scopeProjects, setScopeProjects] = import_react3.default.useState(row.scopeProjects || []);
2531
- const [scopeMode, setScopeMode] = import_react3.default.useState((row.scopeProjects || []).length > 0 ? "projects" : "global");
2711
+ const [scopeProjects, setScopeProjects] = import_react4.default.useState(row.scopeProjects || []);
2712
+ const [scopeMode, setScopeMode] = import_react4.default.useState((row.scopeProjects || []).length > 0 ? "projects" : "global");
2532
2713
  const workspacePaths = new Set(workspaces.map((workspace) => workspace.path));
2533
2714
  const otherProjects = scopeProjects.filter((project) => !workspacePaths.has(project));
2534
2715
  const toggle = (target) => setTargets((prev) => {
@@ -2622,7 +2803,7 @@ function LinksModal({ t, row, agents, workspaces, pickDirectory, onCancel, onSav
2622
2803
  );
2623
2804
  }
2624
2805
  function AutoLinkModal({ t, settings, agents, dsh, onCancel, onSave }) {
2625
- const [targets, setTargets] = import_react3.default.useState(() => new Set(
2806
+ const [targets, setTargets] = import_react4.default.useState(() => new Set(
2626
2807
  Array.isArray(settings.autoLinkTargets) ? settings.autoLinkTargets : agents.map((agent) => agent.target)
2627
2808
  ));
2628
2809
  const toggle = (target) => setTargets((prev) => {
@@ -2678,146 +2859,6 @@ function AutoLinkModal({ t, settings, agents, dsh, onCancel, onSave }) {
2678
2859
  h("p", { className: "dshsc-sub" }, t("autoLink.hint"))
2679
2860
  );
2680
2861
  }
2681
- function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
2682
- const [source, setSource] = import_react3.default.useState("");
2683
- const [phase, setPhase] = import_react3.default.useState("idle");
2684
- const [options, setOptions] = import_react3.default.useState([]);
2685
- const [selected, setSelected] = import_react3.default.useState(() => /* @__PURE__ */ new Set());
2686
- const [manual, setManual] = import_react3.default.useState("");
2687
- const [skipped, setSkipped] = import_react3.default.useState(0);
2688
- const [error, setError] = import_react3.default.useState(null);
2689
- const reset = (value) => {
2690
- setSource(value);
2691
- setPhase("idle");
2692
- setOptions([]);
2693
- setSelected(/* @__PURE__ */ new Set());
2694
- setSkipped(0);
2695
- setError(null);
2696
- };
2697
- const probe = async () => {
2698
- const value = source.trim();
2699
- if (!value || phase === "probing") return;
2700
- setPhase("probing");
2701
- setError(null);
2702
- try {
2703
- const result = await onProbe(value);
2704
- const found = Array.isArray(result?.skills) ? result.skills : [];
2705
- setOptions(found);
2706
- setSelected(new Set(found.map((item) => item.name)));
2707
- setSkipped(Math.max(0, Number(result?.skipped) || 0));
2708
- setPhase(found.length > 0 ? "picked" : "empty");
2709
- } catch (err) {
2710
- setError(err?.message || String(err));
2711
- setPhase("failed");
2712
- }
2713
- };
2714
- const toggle = (name) => setSelected((prev) => {
2715
- const next = new Set(prev);
2716
- if (next.has(name)) next.delete(name);
2717
- else next.add(name);
2718
- return next;
2719
- });
2720
- const names = manual.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
2721
- const skippedText = skipped > 0 ? t("form.skipped").replace("{n}", String(skipped)) : null;
2722
- const manualMode = phase === "empty" || phase === "failed";
2723
- const skills = phase === "picked" ? [...selected] : names;
2724
- const canSubmit = phase === "picked" ? selected.size > 0 : manualMode ? names.length > 0 : false;
2725
- const submit = () => {
2726
- if (!canSubmit) return;
2727
- onInstall({ source: source.trim(), skills });
2728
- };
2729
- return h(
2730
- Overlay,
2731
- {
2732
- t,
2733
- title: t("action.addSkill"),
2734
- onCancel,
2735
- footer: [
2736
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2737
- h("button", {
2738
- key: "s",
2739
- className: "dshsc-btn primary",
2740
- disabled: !canSubmit,
2741
- onClick: submit
2742
- }, t("action.save"))
2743
- ]
2744
- },
2745
- h("p", { className: "dshsc-sub" }, t("form.pickHint")),
2746
- h(
2747
- "div",
2748
- { className: "dshsc-field" },
2749
- h("span", null, t("form.source")),
2750
- h(
2751
- "div",
2752
- { className: "dshsc-inline" },
2753
- h("input", {
2754
- value: source,
2755
- placeholder: t("form.sourceHint"),
2756
- onChange: (event) => reset(event.target.value),
2757
- onKeyDown: (event) => {
2758
- if (event.key === "Enter") void probe();
2759
- }
2760
- }),
2761
- h("button", {
2762
- className: "dshsc-btn",
2763
- disabled: !source.trim() || phase === "probing",
2764
- onClick: () => void probe()
2765
- }, phase === "probing" ? t("form.probing") : t("form.probe"))
2766
- )
2767
- ),
2768
- phase === "picked" ? h(
2769
- "div",
2770
- { key: "picked" },
2771
- h("div", { className: "dshsc-group" }, t("form.detected").replace("{n}", String(options.length))),
2772
- h(
2773
- "div",
2774
- { style: { display: "flex", gap: 6, marginBottom: 6 } },
2775
- h("button", {
2776
- className: "dshsc-btn ghost",
2777
- onClick: () => setSelected(new Set(options.map((item) => item.name)))
2778
- }, t("form.selectAll")),
2779
- h("button", {
2780
- className: "dshsc-btn ghost",
2781
- onClick: () => setSelected(/* @__PURE__ */ new Set())
2782
- }, t("form.selectNone"))
2783
- ),
2784
- h("div", { className: "dshsc-panel" }, options.map((option) => h(
2785
- "label",
2786
- { className: "dshsc-check dshsc-skill", key: option.name },
2787
- h("input", {
2788
- type: "checkbox",
2789
- checked: selected.has(option.name),
2790
- onChange: () => toggle(option.name)
2791
- }),
2792
- h(
2793
- "span",
2794
- { className: "dshsc-skill-main" },
2795
- h("span", { className: "dshsc-skill-name" }, option.name),
2796
- option.description ? h("small", { className: "dshsc-skill-desc" }, option.description) : null,
2797
- h("small", { className: "dshsc-skill-path" }, option.skillPath)
2798
- )
2799
- ))),
2800
- skippedText ? h("p", { className: "dshsc-sub" }, skippedText) : null
2801
- ) : null,
2802
- phase === "probing" ? h("p", { className: "dshsc-sub" }, t("form.probingHint")) : null,
2803
- manualMode ? h(
2804
- "div",
2805
- { key: "manual" },
2806
- h("p", { className: "dshsc-sub" }, phase === "empty" ? skippedText || t("form.noSkills") : `${t("form.probeFailed")}${error ? ` · ${error}` : ""}`),
2807
- h(
2808
- "label",
2809
- { className: "dshsc-field" },
2810
- h("span", null, t("form.skills")),
2811
- h("input", {
2812
- value: manual,
2813
- placeholder: "git-kit, handoff",
2814
- onChange: (event) => setManual(event.target.value)
2815
- })
2816
- )
2817
- ) : null,
2818
- h("p", { className: "dshsc-sub" }, t("form.hint"))
2819
- );
2820
- }
2821
2862
  function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, actions, onClose, onSaveAutoLink }) {
2822
2863
  if (modal.type === "add-skill") {
2823
2864
  return h(AddSkillModal, {
@@ -2900,7 +2941,7 @@ ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
2900
2941
  }
2901
2942
 
2902
2943
  // client/views/skill-reader.js
2903
- var import_react4 = __toESM(require("react"), 1);
2944
+ var import_react5 = __toESM(require("react"), 1);
2904
2945
 
2905
2946
  // client/markdown.js
2906
2947
  function classifyHref(href) {
@@ -3183,7 +3224,7 @@ function resolveRelativeHref(currentDir, href) {
3183
3224
  return { relPath: out.join("/"), anchor: target.kind === "anchor" ? target.href : "" };
3184
3225
  }
3185
3226
 
3186
- // client/views/skill-reader.js
3227
+ // client/views/markdown-view.js
3187
3228
  var PARSE_CACHE = /* @__PURE__ */ new Map();
3188
3229
  function parseCached(text) {
3189
3230
  const key = String(text ?? "");
@@ -3196,49 +3237,6 @@ function parseCached(text) {
3196
3237
  }
3197
3238
  return parsed;
3198
3239
  }
3199
- function formatBytes(bytes) {
3200
- const value = Number(bytes) || 0;
3201
- if (value < 1024) return `${value} B`;
3202
- if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3203
- return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3204
- }
3205
- function formatWhen(iso) {
3206
- if (!iso) return null;
3207
- const date = new Date(iso);
3208
- if (Number.isNaN(date.getTime())) return null;
3209
- const pad = (value) => String(value).padStart(2, "0");
3210
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3211
- }
3212
- var MARKDOWN = /\.(md|markdown)$/i;
3213
- function isMarkdownPath(relPath) {
3214
- return MARKDOWN.test(String(relPath || ""));
3215
- }
3216
- function isScroller(node) {
3217
- if (!node || typeof getComputedStyle !== "function") return false;
3218
- const style = getComputedStyle(node);
3219
- return Boolean(style && /(auto|scroll)/.test(style.overflowY || "") && node.scrollHeight > node.clientHeight + 4);
3220
- }
3221
- function nearestScroller(node) {
3222
- let current = node?.parentElement || null;
3223
- while (current) {
3224
- if (isScroller(current)) return current;
3225
- current = current.parentElement || null;
3226
- }
3227
- return typeof document !== "undefined" ? document.scrollingElement || null : null;
3228
- }
3229
- function scrollHost(node) {
3230
- if (isScroller(node)) return node;
3231
- return node && nearestScroller(node) || node;
3232
- }
3233
- var STATUS_KEY = {
3234
- current: "status.current",
3235
- "update-available": "status.update",
3236
- "locally-modified": "status.local",
3237
- conflict: "status.conflict",
3238
- "upstream-deleted": "status.deleted",
3239
- "broken-link": "status.broken",
3240
- unmanaged: "status.unmanaged"
3241
- };
3242
3240
  function inlineNodes(nodes, { onOpenFile, onOpenAnchor, keyPrefix = "i" }) {
3243
3241
  if (!Array.isArray(nodes)) return null;
3244
3242
  return nodes.map((node, index) => {
@@ -3371,6 +3369,34 @@ function Markdown({ text, onOpenFile, onOpenAnchor }) {
3371
3369
  const { blocks } = parseCached(text);
3372
3370
  return h("div", { className: "dshsc-md" }, blockNodes(blocks, { onOpenFile, onOpenAnchor }));
3373
3371
  }
3372
+
3373
+ // client/views/skill-reader.js
3374
+ function formatBytes(bytes) {
3375
+ const value = Number(bytes) || 0;
3376
+ if (value < 1024) return `${value} B`;
3377
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3378
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3379
+ }
3380
+ function formatWhen(iso) {
3381
+ if (!iso) return null;
3382
+ const date = new Date(iso);
3383
+ if (Number.isNaN(date.getTime())) return null;
3384
+ const pad = (value) => String(value).padStart(2, "0");
3385
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3386
+ }
3387
+ var MARKDOWN = /\.(md|markdown)$/i;
3388
+ function isMarkdownPath(relPath) {
3389
+ return MARKDOWN.test(String(relPath || ""));
3390
+ }
3391
+ var STATUS_KEY = {
3392
+ current: "status.current",
3393
+ "update-available": "status.update",
3394
+ "locally-modified": "status.local",
3395
+ conflict: "status.conflict",
3396
+ "upstream-deleted": "status.deleted",
3397
+ "broken-link": "status.broken",
3398
+ unmanaged: "status.unmanaged"
3399
+ };
3374
3400
  function TabButton({ id, label, count, active, onSelect }) {
3375
3401
  return h("button", {
3376
3402
  type: "button",
@@ -3411,14 +3437,17 @@ function SkillReader({
3411
3437
  onToast
3412
3438
  }) {
3413
3439
  const { state, file, loadFile, retry } = useSkillReader(row);
3414
- const [tab, setTab] = import_react4.default.useState(initialTab);
3415
- const [format, setFormat] = import_react4.default.useState("rendered");
3416
- const [docOverride, setDocOverride] = import_react4.default.useState(null);
3417
- const [fileFormat, setFileFormat] = import_react4.default.useState("rendered");
3418
- const [tocActive, setTocActive] = import_react4.default.useState(null);
3419
- const bodyRef = import_react4.default.useRef(null);
3420
- const dialogRef = import_react4.default.useRef(null);
3421
- const scrollRef = import_react4.default.useRef(0);
3440
+ const [tab, setTab] = import_react5.default.useState(initialTab);
3441
+ const [format, setFormat] = import_react5.default.useState("rendered");
3442
+ const [docOverride, setDocOverride] = import_react5.default.useState(null);
3443
+ const [fileFormat, setFileFormat] = import_react5.default.useState("rendered");
3444
+ const [tocActive, setTocActive] = import_react5.default.useState(null);
3445
+ const [descOpen, setDescOpen] = import_react5.default.useState(false);
3446
+ const [descClamped, setDescClamped] = import_react5.default.useState(false);
3447
+ const bodyRef = import_react5.default.useRef(null);
3448
+ const dialogRef = import_react5.default.useRef(null);
3449
+ const descRef = import_react5.default.useRef(null);
3450
+ const scrollRef = import_react5.default.useRef(0);
3422
3451
  useOverlayLayer(variant === "drawer" || variant === "page", onClose);
3423
3452
  const name = row?.name || "";
3424
3453
  const tree = state.tree;
@@ -3427,20 +3456,49 @@ function SkillReader({
3427
3456
  const bodyText = docOverride ? docOverride.content : state.body?.content || "";
3428
3457
  const description = row?.description || state.body?.meta?.description || "";
3429
3458
  const headings = tab === "doc" && bodyText ? parseCached(bodyText).headings : [];
3430
- import_react4.default.useEffect(() => {
3459
+ import_react5.default.useEffect(() => {
3431
3460
  setDocOverride(null);
3432
3461
  scrollRef.current = 0;
3433
3462
  }, [name]);
3434
- import_react4.default.useEffect(() => {
3463
+ const descOwnerRef = import_react5.default.useRef(name);
3464
+ import_react5.default.useEffect(() => {
3465
+ if (descOwnerRef.current === name) return;
3466
+ descOwnerRef.current = name;
3467
+ setDescOpen(false);
3468
+ setDescClamped(false);
3469
+ }, [name]);
3470
+ import_react5.default.useLayoutEffect(() => {
3471
+ const node = descRef.current;
3472
+ if (!node) return void 0;
3473
+ const measure = () => {
3474
+ if (descOpen) return;
3475
+ const scroll = Number(node.scrollHeight) || 0;
3476
+ const client = Number(node.clientHeight) || 0;
3477
+ setDescClamped(scroll > client + 1);
3478
+ };
3479
+ measure();
3480
+ if (typeof window === "undefined" || !window.addEventListener) return void 0;
3481
+ window.addEventListener("resize", measure);
3482
+ let observer = null;
3483
+ if (typeof ResizeObserver === "function") {
3484
+ observer = new ResizeObserver(measure);
3485
+ observer.observe(node);
3486
+ }
3487
+ return () => {
3488
+ observer?.disconnect?.();
3489
+ window.removeEventListener("resize", measure);
3490
+ };
3491
+ }, [description, descOpen]);
3492
+ import_react5.default.useEffect(() => {
3435
3493
  if (!initialFile) return;
3436
3494
  setTab("files");
3437
3495
  void loadFile(initialFile);
3438
3496
  }, [initialFile, loadFile]);
3439
- import_react4.default.useEffect(() => {
3497
+ import_react5.default.useEffect(() => {
3440
3498
  const node = bodyRef.current;
3441
3499
  if (node && scrollRef.current) node.scrollTop = scrollRef.current;
3442
3500
  });
3443
- import_react4.default.useEffect(() => {
3501
+ import_react5.default.useEffect(() => {
3444
3502
  if (variant !== "drawer" || typeof document === "undefined") return void 0;
3445
3503
  const previous = document.activeElement;
3446
3504
  const node = dialogRef.current;
@@ -3467,12 +3525,12 @@ function SkillReader({
3467
3525
  previous?.focus?.();
3468
3526
  };
3469
3527
  }, [variant, onClose]);
3470
- import_react4.default.useEffect(() => {
3528
+ import_react5.default.useEffect(() => {
3471
3529
  if (variant !== "page" || typeof document === "undefined") return;
3472
3530
  const node = document.getElementById("dshsc-reader-name");
3473
3531
  node?.focus?.();
3474
3532
  }, [variant, name]);
3475
- import_react4.default.useEffect(() => {
3533
+ import_react5.default.useEffect(() => {
3476
3534
  if (variant !== "page" || typeof window === "undefined") return void 0;
3477
3535
  const node = bodyRef.current;
3478
3536
  if (!node?.querySelectorAll) return void 0;
@@ -3542,7 +3600,22 @@ function SkillReader({
3542
3600
  onClick: () => onClose?.()
3543
3601
  }, "×") : null
3544
3602
  ),
3545
- description ? h("p", { className: "dshsc-reader-desc" }, description) : null,
3603
+ description ? h(
3604
+ "div",
3605
+ { className: "dshsc-reader-descblock" },
3606
+ h("p", {
3607
+ className: `dshsc-reader-desc${descOpen ? " expanded" : ""}`,
3608
+ id: "dshsc-reader-desc",
3609
+ ref: descRef
3610
+ }, description),
3611
+ descClamped ? h("button", {
3612
+ type: "button",
3613
+ className: "dshsc-reader-more",
3614
+ "aria-expanded": String(descOpen),
3615
+ "aria-controls": "dshsc-reader-desc",
3616
+ onClick: () => setDescOpen((prev) => !prev)
3617
+ }, t(descOpen ? "action.collapse" : "action.expand")) : null
3618
+ ) : null,
3546
3619
  h(
3547
3620
  "div",
3548
3621
  { className: "dshsc-reader-meta" },
@@ -3835,6 +3908,7 @@ function SkillReader({
3835
3908
  }, t("action.update")) : null
3836
3909
  );
3837
3910
  if (variant === "page") {
3911
+ const tocShown = headings.length > 1;
3838
3912
  return h(
3839
3913
  "div",
3840
3914
  { className: "dshsc-reader-page" },
@@ -3860,8 +3934,8 @@ function SkillReader({
3860
3934
  h("div", { className: "dshsc-reader-hero" }, header),
3861
3935
  h(
3862
3936
  "div",
3863
- { className: "dshsc-reader-layout" },
3864
- headings.length > 1 ? h(
3937
+ { className: `dshsc-reader-layout${tocShown ? " has-toc" : ""}` },
3938
+ tocShown ? h(
3865
3939
  "nav",
3866
3940
  { className: "dshsc-reader-toc", "aria-label": t("viewer.toc") },
3867
3941
  h("div", { className: "dshsc-reader-toc-title" }, t("viewer.toc")),
@@ -3895,19 +3969,19 @@ function SkillReader({
3895
3969
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
3896
3970
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3897
3971
  const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
3898
- const [tab, setTab] = import_react5.default.useState("installed");
3899
- const [view, setView] = import_react5.default.useState("flat");
3900
- const [modal, setModal] = import_react5.default.useState(null);
3901
- const [expanded, setExpanded] = import_react5.default.useState({});
3902
- const [placementBusy, setPlacementBusy] = import_react5.default.useState(null);
3903
- const [placementOverride, setPlacementOverride] = import_react5.default.useState(null);
3904
- const [query, setQuery] = import_react5.default.useState("");
3905
- const [viewer, setViewer] = import_react5.default.useState(null);
3906
- const rootRef = import_react5.default.useRef(null);
3907
- const scrollPos = import_react5.default.useRef(null);
3908
- const restoreScroll = import_react5.default.useRef(null);
3909
- const [filter, setFilter] = import_react5.default.useState("all");
3910
- const [builtinOpen, setBuiltinOpen] = import_react5.default.useState(false);
3972
+ const [tab, setTab] = import_react6.default.useState("installed");
3973
+ const [view, setView] = import_react6.default.useState("flat");
3974
+ const [modal, setModal] = import_react6.default.useState(null);
3975
+ const [expanded, setExpanded] = import_react6.default.useState({});
3976
+ const [placementBusy, setPlacementBusy] = import_react6.default.useState(null);
3977
+ const [placementOverride, setPlacementOverride] = import_react6.default.useState(null);
3978
+ const [query, setQuery] = import_react6.default.useState("");
3979
+ const [viewer, setViewer] = import_react6.default.useState(null);
3980
+ const rootRef = import_react6.default.useRef(null);
3981
+ const scrollPos = import_react6.default.useRef(null);
3982
+ const restoreScroll = import_react6.default.useRef(null);
3983
+ const [filter, setFilter] = import_react6.default.useState("all");
3984
+ const [builtinOpen, setBuiltinOpen] = import_react6.default.useState(false);
3911
3985
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
3912
3986
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
3913
3987
  const rows = data?.installs || [];
@@ -3928,7 +4002,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3928
4002
  const agentLabels = row.links.map((link) => link.target === "dsh-global" ? "DSH" : link.label);
3929
4003
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
3930
4004
  });
3931
- import_react5.default.useEffect(() => {
4005
+ import_react6.default.useEffect(() => {
3932
4006
  if (!viewer || !data) return;
3933
4007
  if (rows.some((row) => row.name === viewer.name)) return;
3934
4008
  setViewer(null);
@@ -3940,23 +4014,10 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3940
4014
  const autoLinkDsh = (data?.agents?.all || []).find((agent) => agent.target === "dsh-global") || null;
3941
4015
  const autoLinkLabel = [t("links.dshGlobal"), ...autoLinkAgents.map((agent) => agent.label)].join("、");
3942
4016
  const placement = (placementOverride ?? data?.settings?.placement) === "sidebar" ? "sidebar" : "settings";
3943
- function getScrollParent(node) {
3944
- let current = node?.parentElement || null;
3945
- while (current) {
3946
- if (typeof getComputedStyle === "function") {
3947
- const style = getComputedStyle(current);
3948
- if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight) {
3949
- return current;
3950
- }
3951
- }
3952
- current = current.parentElement || null;
3953
- }
3954
- return typeof document !== "undefined" ? document.scrollingElement || null : null;
3955
- }
3956
4017
  function listScroller() {
3957
4018
  const node = rootRef.current?.querySelector?.(".dshsc-listscroll");
3958
- if (node && node.scrollHeight > node.clientHeight) return node;
3959
- return getScrollParent(rootRef.current);
4019
+ if (isScroller(node)) return node;
4020
+ return nearestScroller(rootRef.current);
3960
4021
  }
3961
4022
  function openViewer(row, mode = "drawer") {
3962
4023
  if (!row) return;
@@ -3976,7 +4037,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3976
4037
  if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
3977
4038
  setViewer(null);
3978
4039
  }
3979
- import_react5.default.useLayoutEffect(() => {
4040
+ import_react6.default.useLayoutEffect(() => {
3980
4041
  if (restoreScroll.current === null) return;
3981
4042
  const target = restoreScroll.current;
3982
4043
  restoreScroll.current = null;
@@ -4043,7 +4104,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4043
4104
  onToast: (next) => setToast(next)
4044
4105
  }) : null;
4045
4106
  const listNode = h(
4046
- import_react5.default.Fragment,
4107
+ import_react6.default.Fragment,
4047
4108
  null,
4048
4109
  h(Header, {
4049
4110
  t,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-skills-console",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "morehao <morehao@qq.com>",
@@ -52,7 +52,8 @@
52
52
  "build:client": "node scripts/build-client.mjs",
53
53
  "dev:client": "node scripts/build-client.mjs --watch",
54
54
  "check:client": "node scripts/build-client.mjs --check",
55
- "check": "node scripts/check-syntax.mjs && node scripts/build-client.mjs --check && node scripts/check-naming.mjs && node scripts/check-pack.mjs && node scripts/smoke-release-notes.mjs",
55
+ "check:cycles": "node scripts/check-cycles.mjs",
56
+ "check": "node scripts/check-syntax.mjs && node scripts/check-cycles.mjs && node scripts/build-client.mjs --check && node scripts/check-naming.mjs && node scripts/check-pack.mjs && node scripts/smoke-release-notes.mjs",
56
57
  "check:syntax": "node scripts/check-syntax.mjs",
57
58
  "check:naming": "node scripts/check-naming.mjs",
58
59
  "check:pack": "node scripts/check-pack.mjs",