@morehao/dsh-skills-console 0.3.1 → 0.3.3

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
@@ -99,7 +99,9 @@ var DICT = {
99
99
  "view.agent": "按 Agent",
100
100
  autoLink: "自动接入",
101
101
  "settings.title": "插件设置",
102
- "version.label": "当前版本",
102
+ "id.copy": "复制安装标识",
103
+ "id.copied": "已复制安装标识",
104
+ "id.copyFailed": "复制失败,请重试",
103
105
  "placement.label": "侧边栏入口",
104
106
  "placement.effect": "切换后立即生效",
105
107
  "placement.on": "已开启",
@@ -114,6 +116,8 @@ var DICT = {
114
116
  "status.broken": "异常:软链断裂",
115
117
  "status.unmanaged": "未纳入追踪",
116
118
  "status.unknown": "待检查",
119
+ "status.noUpstream": "无上游",
120
+ "status.noUpstreamHint": "没有可用的订阅来源,无法检查更新;重新接管或添加订阅后再试",
117
121
  "status.off": "已停用",
118
122
  "status.brokenHint": "软链目标不存在:重新接入,或删除后重装",
119
123
  "adopt.switchHint": "还未纳入追踪,不能启停",
@@ -200,7 +204,62 @@ var DICT = {
200
204
  visibleOnlyBuiltin: "本插件仅管理 ~/.agents/skills 下的技能,DSH 内置技能不在管理范围内",
201
205
  "builtin.view": "查看内置技能({n})",
202
206
  "builtin.hide": "收起内置技能",
203
- external: "非本插件创建"
207
+ external: "非本插件创建",
208
+ // 查看器:抽屉 / 整页共用的文案
209
+ "viewer.title": "技能内容",
210
+ "viewer.close": "关闭",
211
+ "viewer.openPage": "在整页打开",
212
+ "viewer.back": "返回列表",
213
+ "viewer.tab.doc": "正文",
214
+ "viewer.tab.files": "文件",
215
+ "viewer.tab.info": "信息",
216
+ "viewer.rendered": "渲染",
217
+ "viewer.raw": "原文",
218
+ "viewer.loading": "正在读取 SKILL.md…",
219
+ "viewer.retry": "重试",
220
+ "viewer.copyPath": "复制路径",
221
+ "viewer.copyBody": "复制正文",
222
+ "viewer.copied": "已复制",
223
+ "viewer.copyFailed": "复制失败,请手动选中后复制",
224
+ "viewer.gone": "这个技能已不在本地,查看器已关闭",
225
+ "viewer.noSkill": "这个技能里没有 SKILL.md",
226
+ "viewer.noSkillHint": "DSH 解析不到它,但目录里的其他文件仍可以看",
227
+ "viewer.binary": "暂不支持预览",
228
+ "viewer.binaryHint": "二进制或非文本文件,只显示文件名与大小",
229
+ "viewer.textNote": "只读,直接显示源码",
230
+ "viewer.tooLarge": "超过 {size},只显示前一部分",
231
+ "viewer.emptyFiles": "这个技能目录里没有可读的文件",
232
+ "viewer.fileCount": "{n} 个文件",
233
+ "viewer.size": "{size}",
234
+ "viewer.backToBody": "回到 SKILL.md",
235
+ "viewer.openInDoc": "在正文中打开",
236
+ "viewer.toc": "目录",
237
+ "viewer.readonlyNote": "只读,内容来自本机 SKILL.md",
238
+ "viewer.info.basic": "标识",
239
+ "viewer.info.name": "名称",
240
+ "viewer.info.desc": "描述",
241
+ "viewer.info.path": "本地路径",
242
+ "viewer.info.source": "来源",
243
+ "viewer.info.repo": "仓库",
244
+ "viewer.info.ref": "基准",
245
+ "viewer.info.commit": "commit",
246
+ "viewer.info.skillPath": "skillPath",
247
+ "viewer.info.hash": "内容哈希",
248
+ "viewer.info.installedAt": "安装",
249
+ "viewer.info.updatedAt": "更新",
250
+ "viewer.info.size": "大小",
251
+ "viewer.info.local": "本地",
252
+ "viewer.info.localChange": "本地改动",
253
+ "viewer.info.localChangeNone": "无(与安装时一致)",
254
+ "viewer.info.localChangeYes": "有本地改动",
255
+ "viewer.info.links": "接入",
256
+ "viewer.info.agents": "Agent",
257
+ "viewer.info.scope": "范围",
258
+ "viewer.info.scopeGlobal": "全局",
259
+ "viewer.info.scopeProject": "仅项目",
260
+ "viewer.info.mode": "状态",
261
+ "viewer.info.status": "更新",
262
+ "viewer.info.none": "无"
204
263
  },
205
264
  en: {
206
265
  "section.title": "Skills Console",
@@ -230,7 +289,9 @@ var DICT = {
230
289
  "view.agent": "By agent",
231
290
  "autoLink": "Auto-link",
232
291
  "settings.title": "Plugin settings",
233
- "version.label": "Version",
292
+ "id.copy": "Copy install specifier",
293
+ "id.copied": "Install specifier copied",
294
+ "id.copyFailed": "Copy failed, please retry",
234
295
  "placement.label": "Sidebar entry",
235
296
  "placement.effect": "Applies immediately",
236
297
  "placement.on": "on",
@@ -245,6 +306,8 @@ var DICT = {
245
306
  "status.broken": "Broken link",
246
307
  "status.unmanaged": "Untracked",
247
308
  "status.unknown": "Not checked",
309
+ "status.noUpstream": "No upstream",
310
+ "status.noUpstreamHint": "No usable subscription source, so update checks are impossible; re-adopt or add a subscription",
248
311
  "status.off": "Disabled",
249
312
  "status.brokenHint": "The link target is missing: relink, or delete and reinstall",
250
313
  "adopt.switchHint": "Not tracked yet, cannot be enabled or disabled",
@@ -331,9 +394,75 @@ var DICT = {
331
394
  "builtin.view": "View built-ins ({n})",
332
395
  "builtin.hide": "Hide built-ins",
333
396
  external: "created outside this plugin",
334
- "adopt.done": "now tracked"
397
+ "adopt.done": "now tracked",
398
+ // Viewer: shared by the drawer and the full page
399
+ "viewer.title": "Skill content",
400
+ "viewer.close": "Close",
401
+ "viewer.openPage": "Open as page",
402
+ "viewer.back": "Back to list",
403
+ "viewer.tab.doc": "Content",
404
+ "viewer.tab.files": "Files",
405
+ "viewer.tab.info": "Info",
406
+ "viewer.rendered": "Rendered",
407
+ "viewer.raw": "Raw",
408
+ "viewer.loading": "Reading SKILL.md…",
409
+ "viewer.retry": "Retry",
410
+ "viewer.copyPath": "Copy path",
411
+ "viewer.copyBody": "Copy content",
412
+ "viewer.copied": "Copied",
413
+ "viewer.copyFailed": "Copy failed, select the text manually",
414
+ "viewer.gone": "This skill is gone from disk; the viewer was closed",
415
+ "viewer.noSkill": "This skill has no SKILL.md",
416
+ "viewer.noSkillHint": "DSH cannot parse it, but the other files are still readable",
417
+ "viewer.binary": "Preview not supported",
418
+ "viewer.binaryHint": "Binary or non-text file: only the name and size are shown",
419
+ "viewer.textNote": "Read-only source",
420
+ "viewer.tooLarge": "Larger than {size}; showing the first part only",
421
+ "viewer.emptyFiles": "No readable files in this skill folder",
422
+ "viewer.fileCount": "{n} files",
423
+ "viewer.size": "{size}",
424
+ "viewer.backToBody": "Back to SKILL.md",
425
+ "viewer.openInDoc": "Open in content",
426
+ "viewer.toc": "Contents",
427
+ "viewer.readonlyNote": "Read-only, content comes from the local SKILL.md",
428
+ "viewer.info.basic": "Identity",
429
+ "viewer.info.name": "Name",
430
+ "viewer.info.desc": "Description",
431
+ "viewer.info.path": "Local path",
432
+ "viewer.info.source": "Origin",
433
+ "viewer.info.repo": "Repository",
434
+ "viewer.info.ref": "Baseline",
435
+ "viewer.info.commit": "commit",
436
+ "viewer.info.skillPath": "skillPath",
437
+ "viewer.info.hash": "Content hash",
438
+ "viewer.info.installedAt": "Installed",
439
+ "viewer.info.updatedAt": "Updated",
440
+ "viewer.info.size": "Size",
441
+ "viewer.info.local": "Local",
442
+ "viewer.info.localChange": "Local changes",
443
+ "viewer.info.localChangeNone": "None (matches install)",
444
+ "viewer.info.localChangeYes": "Modified locally",
445
+ "viewer.info.links": "Linked",
446
+ "viewer.info.agents": "Agents",
447
+ "viewer.info.scope": "Scope",
448
+ "viewer.info.scopeGlobal": "Global",
449
+ "viewer.info.scopeProject": "Project only",
450
+ "viewer.info.mode": "State",
451
+ "viewer.info.status": "Updates",
452
+ "viewer.info.none": "None"
335
453
  }
336
454
  };
455
+ async function copyText(text) {
456
+ const value = String(text ?? "");
457
+ if (!value) return false;
458
+ try {
459
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) return false;
460
+ await navigator.clipboard.writeText(value);
461
+ return true;
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
337
466
  function buildTranslate(locale) {
338
467
  const active = () => {
339
468
  try {
@@ -617,9 +746,89 @@ function SearchIcon({ size = 13 }) {
617
746
  h("path", { d: "M16 16l4.3 4.3" })
618
747
  );
619
748
  }
749
+ var STROKE_ICON = {
750
+ fill: "none",
751
+ stroke: "currentColor",
752
+ strokeWidth: 1.8,
753
+ strokeLinecap: "round",
754
+ strokeLinejoin: "round"
755
+ };
756
+ function EyeIcon({ size = 13 }) {
757
+ return h(
758
+ "svg",
759
+ {
760
+ width: size,
761
+ height: size,
762
+ viewBox: "0 0 24 24",
763
+ ...STROKE_ICON,
764
+ "aria-hidden": true,
765
+ style: { display: "block" }
766
+ },
767
+ h("path", { d: "M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z" }),
768
+ h("circle", { cx: 12, cy: 12, r: 3 })
769
+ );
770
+ }
771
+ function CopyIcon({ size = 13 }) {
772
+ return h(
773
+ "svg",
774
+ {
775
+ width: size,
776
+ height: size,
777
+ viewBox: "0 0 24 24",
778
+ ...STROKE_ICON,
779
+ "aria-hidden": true,
780
+ style: { display: "block" }
781
+ },
782
+ h("rect", { x: 8, y: 8, width: 14, height: 14, rx: 2 }),
783
+ h("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
784
+ );
785
+ }
786
+ function ChevronLeftIcon({ size = 14 }) {
787
+ return h(
788
+ "svg",
789
+ {
790
+ width: size,
791
+ height: size,
792
+ viewBox: "0 0 24 24",
793
+ ...STROKE_ICON,
794
+ "aria-hidden": true,
795
+ style: { display: "block" }
796
+ },
797
+ h("path", { d: "m15 18-6-6 6-6" })
798
+ );
799
+ }
800
+ function FileIcon({ size = 12 }) {
801
+ return h(
802
+ "svg",
803
+ {
804
+ width: size,
805
+ height: size,
806
+ viewBox: "0 0 24 24",
807
+ ...STROKE_ICON,
808
+ "aria-hidden": true,
809
+ style: { display: "block", flex: "none" }
810
+ },
811
+ h("path", { d: "M6 3.5h7l5 5V20a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 5 20V5a1.5 1.5 0 0 1 1-1.5z" }),
812
+ h("path", { d: "M13 3.5V9h5" })
813
+ );
814
+ }
815
+ function FolderIcon({ size = 12 }) {
816
+ return h(
817
+ "svg",
818
+ {
819
+ width: size,
820
+ height: size,
821
+ viewBox: "0 0 24 24",
822
+ ...STROKE_ICON,
823
+ "aria-hidden": true,
824
+ style: { display: "block", flex: "none" }
825
+ },
826
+ h("path", { d: "M3.5 7.5A1.5 1.5 0 0 1 5 6h4l1.8 2.2H19a1.5 1.5 0 0 1 1.5 1.5V18A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z" })
827
+ );
828
+ }
620
829
 
621
830
  // client/section.js
622
- var import_react4 = __toESM(require("react"), 1);
831
+ var import_react5 = __toESM(require("react"), 1);
623
832
 
624
833
  // client/styles.js
625
834
  var CSS = `
@@ -641,13 +850,19 @@ var CSS = `
641
850
  .dshsc-head-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-left: auto; }
642
851
  .dshsc-title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
643
852
 
644
- /* 身份:包名 + 版本合成一枚低调 chip,不再各占一段 */
645
- .dshsc-id { display: inline-flex; align-items: center; gap: 6px; min-width: 0; height: 22px; padding: 0 7px;
646
- border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); }
647
- .dshsc-pkg { font-size: 11px; line-height: 1; color: var(--dsw-alias-label-tertiary); white-space: nowrap;
648
- overflow: hidden; text-overflow: ellipsis; font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); }
649
- .dshsc-idsep { flex: none; font-size: 10px; color: var(--dsw-alias-border-l3); }
650
- .dshsc-ver { flex: none; font-size: 11px; line-height: 1; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
853
+ /* 身份:按「安装时输入的包名」显示(@scope/name@version),等宽 + 可点复制 */
854
+ .dshsc-id { display: inline-flex; align-items: center; gap: 5px; box-sizing: border-box; min-width: 0;
855
+ max-width: 100%; height: 22px; padding: 0 6px 0 7px; border: 1px solid var(--dsw-alias-border-l1);
856
+ border-radius: 6px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-tertiary);
857
+ font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);
858
+ font-size: 11px; line-height: 1; white-space: nowrap; cursor: pointer;
859
+ transition: background .12s ease, border-color .12s ease, color .12s ease; }
860
+ .dshsc-id:hover { border-color: var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2);
861
+ color: var(--dsw-alias-label-primary); }
862
+ .dshsc-id:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
863
+ .dshsc-spec { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
864
+ .dshsc-idcopy { display: inline-flex; flex: none; opacity: .5; transition: opacity .12s ease; }
865
+ .dshsc-id:hover .dshsc-idcopy, .dshsc-id:focus-visible .dshsc-idcopy { opacity: 1; }
651
866
 
652
867
  /* 插件级工具(设置 / GitHub):跟在身份胶囊后面,用细线分组;鬼影按钮不和业务按钮抢权重 */
653
868
  .dshsc-tools { display: inline-flex; align-items: center; gap: 2px; flex: none; margin-left: 2px;
@@ -745,7 +960,7 @@ var CSS = `
745
960
 
746
961
  .dshsc-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px;
747
962
  background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
748
- .dshsc-row { display: grid; grid-template-columns: 300px minmax(0, 1fr) 218px; gap: 14px; align-items: center;
963
+ .dshsc-row { display: grid; grid-template-columns: 300px minmax(0, 1fr) 268px; gap: 14px; align-items: center;
749
964
  padding: 8px 12px 8px 14px; min-height: 54px; border-top: 1px solid var(--dsw-alias-border-l1);
750
965
  transition: background .12s ease; }
751
966
  .dshsc-list > .dshsc-row:first-child { border-top: 0; }
@@ -755,6 +970,11 @@ var CSS = `
755
970
  .dshsc-namerow { display: flex; align-items: center; gap: 7px; min-width: 0; }
756
971
  .dshsc-name { font-size: 13px; font-weight: 600; letter-spacing: -.005em; min-width: 0;
757
972
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
973
+ /* 技能名本身是查看入口:清掉 button 默认外观,保留 hover 下划线提示可点 */
974
+ .dshsc-namebtn { border: 0; background: none; padding: 0; font: inherit; font-size: 13px; font-weight: 600;
975
+ letter-spacing: -.005em; color: inherit; text-align: left; cursor: pointer; }
976
+ .dshsc-namebtn:hover { text-decoration: underline; text-underline-offset: 2px; }
977
+ .dshsc-namebtn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; border-radius: 4px; }
758
978
  .dshsc-src { display: flex; align-items: center; gap: 5px; min-width: 0; font-size: 11.5px; line-height: 16px;
759
979
  color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
760
980
  .dshsc-src .dshsc-code { color: var(--dsw-alias-label-secondary); font-size: 11px; }
@@ -950,6 +1170,215 @@ var CSS = `
950
1170
  .dshsc-panel .dshsc-check.locked:hover { background: transparent; }
951
1171
 
952
1172
  .dshsc-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
1173
+ /* ---------- 查看器:抽屉(B1)/ 整页(A1)共用 ---------- */
1174
+ .dshsc-drawer-wrap { position: fixed; inset: 0; z-index: 55; }
1175
+ .dshsc-drawer-scrim { position: absolute; inset: 0; background: rgba(4,6,9,.55); }
1176
+ .dshsc-drawer { position: absolute; top: 0; right: 0; bottom: 0; width: min(700px, 100vw); display: flex;
1177
+ flex-direction: column; min-height: 0; background: var(--dsw-alias-bg-layer-1);
1178
+ border-left: 1px solid var(--dsw-alias-border-l2); box-shadow: -30px 0 70px rgba(0,0,0,.38); outline: none; }
1179
+ .dshsc-reader-head { flex: none; padding: 15px 20px 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
1180
+ .dshsc-reader-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
1181
+ .dshsc-reader-title h2 { flex: 1; min-width: 0; margin: 0; font-size: 18px; line-height: 1.3; font-weight: 650;
1182
+ letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1183
+ .dshsc-reader-close { flex: none; width: 28px; height: 28px; border: 0; border-radius: 7px; background: transparent;
1184
+ color: var(--dsw-alias-label-tertiary); font-size: 18px; line-height: 1; cursor: pointer; }
1185
+ .dshsc-reader-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1186
+ .dshsc-reader-close:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1187
+ .dshsc-reader-desc { margin: 7px 0 0; font-size: 13px; line-height: 1.6; color: var(--dsw-alias-label-secondary);
1188
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
1189
+ .dshsc-reader-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px;
1190
+ font-size: 11.5px; color: var(--dsw-alias-label-tertiary); }
1191
+ .dshsc-reader-tabs { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 20px;
1192
+ border-bottom: 1px solid var(--dsw-alias-border-l1); }
1193
+ .dshsc-rtabs { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--dsw-alias-border-l1);
1194
+ border-radius: 9px; background: var(--dsw-alias-bg-layer-1); }
1195
+ .dshsc-rtab { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 10px; border: 0;
1196
+ border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px;
1197
+ cursor: pointer; white-space: nowrap; }
1198
+ .dshsc-rtab:hover { color: var(--dsw-alias-label-primary); }
1199
+ .dshsc-rtab.active { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary);
1200
+ box-shadow: 0 0 0 1px var(--dsw-alias-border-l1), 0 1px 2px rgba(0,0,0,.12); }
1201
+ .dshsc-rtab .n { min-width: 15px; height: 15px; padding: 0 4px; box-sizing: border-box; border-radius: 999px;
1202
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 20%, transparent); color: var(--dsw-alias-label-secondary);
1203
+ font-size: 10px; line-height: 15px; text-align: center; font-style: normal; font-variant-numeric: tabular-nums; }
1204
+ .dshsc-rtab:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
1205
+ .dshsc-reader-seg { margin-left: auto; }
1206
+ .dshsc-reader-seg .dshsc-btn { height: 22px; font-size: 11.5px; padding: 0 8px; }
1207
+
1208
+ .dshsc-reader-body { flex: 1; min-height: 0; overflow: auto; padding: 16px 20px 22px; outline: none; }
1209
+ .dshsc-reader-doc { display: flex; flex-direction: column; gap: 10px; }
1210
+ .dshsc-reader-filebar { display: flex; align-items: center; gap: 10px; font-size: 11.5px;
1211
+ color: var(--dsw-alias-label-tertiary); }
1212
+ .dshsc-reader-filebar .dshsc-btn { margin-left: auto; height: 24px; font-size: 11.5px; }
1213
+ .dshsc-reader-raw { margin: 0; padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px;
1214
+ background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); overflow: auto;
1215
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.65; white-space: pre-wrap;
1216
+ word-break: break-word; }
1217
+ .dshsc-reader-actions { flex: none; display: flex; align-items: center; gap: 7px; padding: 9px 20px;
1218
+ border-top: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); flex-wrap: wrap; }
1219
+ /* 底栏按钮统一 28px 高;图标按钮做成 28×28 的方正位,避免图标在方框里偏 */
1220
+ .dshsc-reader-actions .dshsc-btn { box-sizing: border-box; height: 28px; font-size: 12px;
1221
+ display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
1222
+ .dshsc-reader-actions .dshsc-btn.sq { width: 28px; padding: 0; }
1223
+ .dshsc-reader-actions .dshsc-btn.sq svg { width: 14px; height: 14px; }
1224
+ .dshsc-reader-note { margin-right: auto; font-size: 11.5px; color: var(--dsw-alias-label-tertiary); }
1225
+ .dshsc-reader-skeleton { display: flex; flex-direction: column; gap: 8px; }
1226
+ .dshsc-reader-skeleton i { display: block; height: 12px; border-radius: 5px;
1227
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 16%, transparent); }
1228
+ .dshsc-reader-skeleton i:nth-child(3) { width: 82%; }
1229
+ .dshsc-reader-skeleton i:nth-child(4) { width: 64%; }
1230
+ .dshsc-reader-skeleton span { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
1231
+ .dshsc-reader-placeholder { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 22px;
1232
+ border: 1px dashed var(--dsw-alias-border-l2); border-radius: 12px;
1233
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 55%, transparent); font-size: 12.5px;
1234
+ color: var(--dsw-alias-label-secondary); line-height: 1.6; }
1235
+ .dshsc-reader-placeholder b { color: var(--dsw-alias-label-primary); font-size: 13px; }
1236
+
1237
+ /* 文件 tab:左树右预览;抽屉里上下排 */
1238
+ .dshsc-reader-files { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
1239
+ .dshsc-reader-tree { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: auto;
1240
+ max-height: 300px; background: var(--dsw-alias-bg-layer-1); }
1241
+ .dshsc-reader-preview { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: hidden;
1242
+ background: var(--dsw-alias-bg-layer-1); }
1243
+ .dshsc-reader-tree-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 8px;
1244
+ padding: 7px 10px; background: var(--dsw-alias-bg-layer-2); border-bottom: 1px solid var(--dsw-alias-border-l1);
1245
+ font-size: 11px; color: var(--dsw-alias-label-tertiary); }
1246
+ .dshsc-reader-tree-head .mono { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
1247
+ .dshsc-reader-tree-head .dshsc-btn { height: 22px; font-size: 11px; padding: 0 7px; }
1248
+ /* 预览头的动作组:整组贴右,组内紧挨(渲染 / 原文 + 在正文中打开) */
1249
+ .dshsc-reader-tools { margin-left: auto; flex: none; display: flex; align-items: center; gap: 6px; }
1250
+ .dshsc-reader-tools:empty { display: none; }
1251
+ .dshsc-reader-tools .dshsc-segment { height: 22px; }
1252
+ .dshsc-reader-tools .dshsc-seg { margin-left: 0; }
1253
+ /* 预览头在深色 bg-layer-2 上,默认边框太淡:抬高一级,让它一眼是按钮 */
1254
+ .dshsc-reader-tools > .dshsc-btn { border: 1px solid var(--dsw-alias-border-l2);
1255
+ background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-weight: 500; }
1256
+ .dshsc-reader-tools > .dshsc-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-3);
1257
+ border-color: var(--dsw-alias-border-l3); }
1258
+ .dshsc-reader-tools > .dshsc-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1259
+ .dshsc-reader-filebar .dshsc-btn { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); }
1260
+ .dshsc-reader-tdir { display: flex; align-items: center; gap: 6px; padding: 5px 10px; font-size: 11.5px;
1261
+ color: var(--dsw-alias-label-tertiary); }
1262
+ .dshsc-reader-tdir .n { margin-left: auto; font-variant-numeric: tabular-nums; }
1263
+ .dshsc-reader-titem { display: flex; align-items: center; gap: 6px; width: 100%; box-sizing: border-box;
1264
+ padding: 5px 10px; border: 0; background: transparent; color: var(--dsw-alias-label-secondary);
1265
+ font: inherit; font-size: 12px; text-align: left; cursor: pointer; }
1266
+ .dshsc-reader-titem:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1267
+ .dshsc-reader-titem.on { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);
1268
+ color: var(--dsw-alias-brand-primary); }
1269
+ .dshsc-reader-titem:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
1270
+ .dshsc-reader-titem .nname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; }
1271
+ .dshsc-reader-titem .nsize { flex: none; font-size: 10.5px; color: var(--dsw-alias-label-tertiary);
1272
+ font-variant-numeric: tabular-nums; }
1273
+ .dshsc-reader-preview .dshsc-reader-raw { border: 0; border-radius: 0; background: transparent;
1274
+ max-height: 320px; }
1275
+ /* 文件 tab 里能渲染的文件(markdown)直接出正文,不让人先读源码 */
1276
+ .dshsc-reader-preview-doc { padding: 12px 14px; max-height: 320px; overflow: auto; }
1277
+ .dshsc-reader-preview-doc .dshsc-md { font-size: 12.5px; }
1278
+ .dshsc-reader-preview-doc .dshsc-md h1.dshsc-md-h { font-size: 18px; }
1279
+ .dshsc-reader-preview-doc .dshsc-md h2.dshsc-md-h { font-size: 15px; }
1280
+
1281
+ /* 信息 tab:四段 dl */
1282
+ .dshsc-reader-info { display: flex; flex-direction: column; gap: 14px; }
1283
+ .dshsc-info-sec { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 11px 13px;
1284
+ background: var(--dsw-alias-bg-layer-1); }
1285
+ .dshsc-info-sec h3 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .04em;
1286
+ color: var(--dsw-alias-label-tertiary); text-transform: uppercase; }
1287
+ .dshsc-info-sec dl { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 5px 10px; margin: 0;
1288
+ font-size: 12px; line-height: 1.55; }
1289
+ .dshsc-info-sec dt { color: var(--dsw-alias-label-tertiary); }
1290
+ .dshsc-info-sec dd { margin: 0; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
1291
+ .dshsc-info-sec dd.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
1292
+
1293
+ /* 正文排版:只用 token,不写死深浅色 */
1294
+ .dshsc-md { font-size: 13.5px; line-height: 1.72; color: var(--dsw-alias-label-primary); }
1295
+ .dshsc-md-h { margin: 20px 0 8px; line-height: 1.35; font-weight: 650; letter-spacing: -.008em; }
1296
+ .dshsc-md-h:first-child { margin-top: 0; }
1297
+ .dshsc-md h1.dshsc-md-h { font-size: 21px; }
1298
+ .dshsc-md h2.dshsc-md-h { font-size: 16.5px; padding-top: 10px; border-top: 1px solid var(--dsw-alias-border-l1); }
1299
+ .dshsc-md h3.dshsc-md-h { font-size: 14px; }
1300
+ .dshsc-md h4.dshsc-md-h, .dshsc-md h5.dshsc-md-h, .dshsc-md h6.dshsc-md-h { font-size: 13px;
1301
+ color: var(--dsw-alias-label-secondary); }
1302
+ .dshsc-md-p { margin: 0 0 10px; }
1303
+ .dshsc-md-inline { padding: 1px 5px; border-radius: 5px; background: var(--dsw-alias-bg-layer-3);
1304
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
1305
+ .dshsc-md-code { margin: 0 0 12px; padding: 11px 13px; border-radius: 9px; overflow: auto;
1306
+ background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1);
1307
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.65; white-space: pre; }
1308
+ .dshsc-md-list { margin: 0 0 10px; padding-left: 20px; }
1309
+ .dshsc-md-list .dshsc-md-list { margin: 4px 0 0; }
1310
+ .dshsc-md-list li { margin-bottom: 4px; }
1311
+ .dshsc-md-task { list-style: none; margin-left: -18px; }
1312
+ .dshsc-md-check { display: inline-block; width: 12px; height: 12px; margin-right: 6px; border-radius: 3px;
1313
+ border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); vertical-align: -1px; }
1314
+ .dshsc-md-check.on { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); }
1315
+ .dshsc-md-quote { margin: 0 0 12px; padding: 6px 0 6px 12px; border-left: 2px solid var(--dsw-alias-border-l3);
1316
+ color: var(--dsw-alias-label-secondary); }
1317
+ .dshsc-md-quote .dshsc-md-p:last-child { margin-bottom: 0; }
1318
+ .dshsc-md-hr { height: 0; margin: 16px 0; border: 0; border-top: 1px solid var(--dsw-alias-border-l1); }
1319
+ .dshsc-md-link { color: var(--dsw-alias-brand-primary); text-decoration: none; }
1320
+ .dshsc-md-link:hover { text-decoration: underline; }
1321
+ .dshsc-md-nolink { color: inherit; border-bottom: 1px dotted var(--dsw-alias-border-l3); }
1322
+ .dshsc-md-image { display: inline-flex; align-items: center; gap: 5px; padding: 2px 7px; border-radius: 6px;
1323
+ border: 1px dashed var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font-size: 11.5px; }
1324
+ .dshsc-md-image-tag { font-size: 9.5px; letter-spacing: .06em; }
1325
+ .dshsc-md-table { margin: 0 0 12px; overflow: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 9px; }
1326
+ .dshsc-md-table table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
1327
+ .dshsc-md-table th, .dshsc-md-table td { padding: 7px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1);
1328
+ text-align: left; }
1329
+ .dshsc-md-table th { background: var(--dsw-alias-bg-layer-2); font-weight: 600; }
1330
+ .dshsc-md-table tr:last-child td { border-bottom: 0; }
1331
+
1332
+ /* 整页(A1):返回条 + hero + 目录 + 720px 正文 */
1333
+ .dshsc-reader-page { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
1334
+ .dshsc-reader-topbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1335
+ .dshsc-reader-topbar .dshsc-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px;
1336
+ box-sizing: border-box; height: 28px; padding: 0 10px; font-size: 12px; line-height: 1; white-space: nowrap; }
1337
+ .dshsc-reader-topbar .dshsc-btn.ghost { padding: 0 9px; }
1338
+ .dshsc-reader-topbar .dshsc-btn.sq { width: 28px; padding: 0; }
1339
+ .dshsc-reader-topbar .dshsc-btn.sq svg { width: 14px; height: 14px; }
1340
+ .dshsc-reader-crumb { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px;
1341
+ color: var(--dsw-alias-label-tertiary); }
1342
+ .dshsc-reader-crumb .sep { color: var(--dsw-alias-border-l3); }
1343
+ .dshsc-reader-topbar .spacer { flex: 1; }
1344
+ /* 顶栏复用抽屉底栏那份动作组:去掉边框 / 底色 / 内边距,只留一排等高的按钮 */
1345
+ .dshsc-reader-topbar .dshsc-reader-actions { border: 0; background: transparent; padding: 0; gap: 6px;
1346
+ flex-wrap: nowrap; }
1347
+ .dshsc-reader-topbar .dshsc-reader-note { display: none; }
1348
+ .dshsc-reader-topbar .dshsc-reader-actions .dshsc-btn:not(.ghost) { padding: 0 11px; }
1349
+ .dshsc-reader-hero { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px;
1350
+ background: var(--dsw-alias-bg-layer-1); }
1351
+ .dshsc-reader-hero .dshsc-reader-head { border-bottom: 0; }
1352
+ .dshsc-reader-hero .dshsc-reader-desc { -webkit-line-clamp: 3; max-width: 78ch; }
1353
+ .dshsc-reader-layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 18px; align-items: start; }
1354
+ .dshsc-reader-toc { position: sticky; top: 0; display: flex; flex-direction: column; gap: 1px;
1355
+ max-height: 70vh; overflow: auto; padding-right: 4px; }
1356
+ .dshsc-reader-toc-title { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase;
1357
+ color: var(--dsw-alias-label-tertiary); margin-bottom: 5px; }
1358
+ .dshsc-reader-tocitem { border: 0; background: transparent; padding: 3px 7px; border-radius: 6px; text-align: left;
1359
+ font: inherit; font-size: 11.5px; line-height: 1.45; color: var(--dsw-alias-label-secondary); cursor: pointer;
1360
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1361
+ .dshsc-reader-tocitem.l2 { padding-left: 15px; }
1362
+ .dshsc-reader-tocitem.l3, .dshsc-reader-tocitem.l4 { padding-left: 23px; font-size: 11px; }
1363
+ .dshsc-reader-tocitem:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1364
+ .dshsc-reader-tocitem.on { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);
1365
+ color: var(--dsw-alias-brand-primary); }
1366
+ .dshsc-reader-tocitem:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
1367
+ .dshsc-reader-col { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--dsw-alias-border-l1);
1368
+ border-radius: 12px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
1369
+ .dshsc-reader-col .dshsc-reader-tabs { border-top: 0; }
1370
+ .dshsc-reader-page .dshsc-reader-body { max-height: none; }
1371
+ .dshsc-reader-page .dshsc-md { max-width: 720px; }
1372
+ .dshsc-reader-page .dshsc-reader-actions { border-radius: 0 0 12px 12px; }
1373
+
1374
+ @container dshsc (max-width: 900px) {
1375
+ /* 行内「查看」先让位:技能名本身仍是入口 */
1376
+ .dshsc-row .dshsc-view { display: none; }
1377
+ .dshsc-reader-layout { grid-template-columns: minmax(0, 1fr); }
1378
+ .dshsc-reader-toc { display: none; }
1379
+ .dshsc-reader-page .dshsc-reader-desc { -webkit-line-clamp: 2; }
1380
+ }
1381
+
953
1382
  .dshsc-toast { position: fixed; right: 18px; bottom: 18px; z-index: 70; font-size: 12px; padding: 10px 13px; border-radius: 9px;
954
1383
  background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2);
955
1384
  box-shadow: 0 8px 24px rgba(0,0,0,.22); max-width: 420px; }
@@ -1260,8 +1689,80 @@ function useAgentTip() {
1260
1689
  }
1261
1690
  return { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip };
1262
1691
  }
1692
+ function useSkillReader(row) {
1693
+ const name = row?.name || null;
1694
+ const stamp = row ? [row.commit || "", row.currentHash || "", row.updatedAt || "", row.status || ""].join("|") : "";
1695
+ const [state, setState] = import_react2.default.useState({ loading: true, body: null, tree: null, bodyError: null, treeError: null });
1696
+ const [file, setFile] = import_react2.default.useState({ relPath: null, content: "", size: 0, truncated: false, loading: false, error: null });
1697
+ const [tick, setTick] = import_react2.default.useState(0);
1698
+ const seq = import_react2.default.useRef(0);
1699
+ const fileSeq = import_react2.default.useRef(0);
1700
+ import_react2.default.useEffect(() => {
1701
+ if (!name) {
1702
+ setState({ loading: false, body: null, tree: null, bodyError: null, treeError: null });
1703
+ return;
1704
+ }
1705
+ const ticket = seq.current + 1;
1706
+ seq.current = ticket;
1707
+ setState({ loading: true, body: null, tree: null, bodyError: null, treeError: null });
1708
+ const encoded = encodeURIComponent(name);
1709
+ void (async () => {
1710
+ const [bodyResult, treeResult] = await Promise.allSettled([
1711
+ apiCall("GET", `/skills/${encoded}`),
1712
+ apiCall("GET", `/skills/${encoded}/files`)
1713
+ ]);
1714
+ if (ticket !== seq.current) return;
1715
+ setState({
1716
+ loading: false,
1717
+ body: bodyResult.status === "fulfilled" ? bodyResult.value : null,
1718
+ tree: treeResult.status === "fulfilled" ? treeResult.value : null,
1719
+ // 没有 SKILL.md 时正文失败但树还在:文件 tab 仍可用
1720
+ bodyError: bodyResult.status === "rejected" ? { code: bodyResult.reason?.code || null, message: bodyResult.reason?.message || "读取失败" } : null,
1721
+ treeError: treeResult.status === "rejected" ? { code: treeResult.reason?.code || null, message: treeResult.reason?.message || "读取失败" } : null
1722
+ });
1723
+ })();
1724
+ }, [name, stamp, tick]);
1725
+ const loadFile = import_react2.default.useCallback(async (relPath) => {
1726
+ if (!name || !relPath) return;
1727
+ const ticket = fileSeq.current + 1;
1728
+ fileSeq.current = ticket;
1729
+ setFile({ relPath, content: "", size: 0, truncated: false, loading: true, error: null });
1730
+ try {
1731
+ const result = await apiCall("GET", `/skills/${encodeURIComponent(name)}?path=${encodeURIComponent(relPath)}`);
1732
+ if (ticket !== fileSeq.current) return;
1733
+ setFile({
1734
+ relPath: result.relPath || relPath,
1735
+ content: result.content || "",
1736
+ size: result.size || 0,
1737
+ truncated: Boolean(result.truncated),
1738
+ loading: false,
1739
+ error: null,
1740
+ mtime: result.mtime || null
1741
+ });
1742
+ } catch (error) {
1743
+ if (ticket !== fileSeq.current) return;
1744
+ setFile({
1745
+ relPath,
1746
+ content: "",
1747
+ size: 0,
1748
+ truncated: false,
1749
+ loading: false,
1750
+ error: { code: error?.code || null, message: error?.message || "读取失败" }
1751
+ });
1752
+ }
1753
+ }, [name]);
1754
+ const retry = import_react2.default.useCallback(() => {
1755
+ setTick((value) => value + 1);
1756
+ }, []);
1757
+ return { state, file, loadFile, retry };
1758
+ }
1263
1759
 
1264
1760
  // client/views/header.js
1761
+ function installSpec(plugin) {
1762
+ const name = plugin?.fullName || plugin?.name;
1763
+ if (!name) return null;
1764
+ return plugin.version ? `${name}@${plugin.version}` : name;
1765
+ }
1265
1766
  function Header({
1266
1767
  t,
1267
1768
  page,
@@ -1280,8 +1781,14 @@ function Header({
1280
1781
  onChangePlacement,
1281
1782
  onOpenAutoLink,
1282
1783
  onAddSkill,
1283
- onAddSubscription
1784
+ onAddSubscription,
1785
+ onToast
1284
1786
  }) {
1787
+ const spec = installSpec(plugin);
1788
+ async function copySpec() {
1789
+ const ok = await copyText(spec);
1790
+ onToast?.(ok ? { kind: "ok", text: t("id.copied") } : { kind: "err", text: t("id.copyFailed") });
1791
+ }
1285
1792
  const headTools = h(
1286
1793
  "div",
1287
1794
  { className: "dshsc-tools" },
@@ -1353,17 +1860,18 @@ function Header({
1353
1860
  "div",
1354
1861
  { className: "dshsc-title-row" },
1355
1862
  h("h2", { className: "dshsc-title" }, page ? t("page.title") : t("section.title")),
1356
- // 身份:包名 + 版本合成一枚低调 chip,由 Host 从 package.json 注入
1357
- plugin?.name ? h(
1358
- "span",
1359
- { className: "dshsc-id" },
1360
- h("span", { className: "dshsc-pkg" }, plugin.name),
1361
- plugin.version ? h("span", { className: "dshsc-idsep" }, "|") : null,
1362
- plugin.version ? h(
1363
- "span",
1364
- { className: "dshsc-ver", title: `${t("version.label")} v${plugin.version}` },
1365
- `v${plugin.version}`
1366
- ) : null
1863
+ // 身份:按「安装时输入的包名」显示,点一下整段复制(Host 从 package.json 注入)
1864
+ spec ? h(
1865
+ "button",
1866
+ {
1867
+ type: "button",
1868
+ className: "dshsc-id",
1869
+ title: `${t("id.copy")}:${spec}`,
1870
+ "aria-label": `${t("id.copy")}:${spec}`,
1871
+ onClick: () => void copySpec()
1872
+ },
1873
+ h("span", { className: "dshsc-spec" }, spec),
1874
+ h("span", { className: "dshsc-idcopy", "aria-hidden": "true" }, h(CopyIcon, { size: 11 }))
1367
1875
  ) : null,
1368
1876
  headTools
1369
1877
  ),
@@ -1472,6 +1980,9 @@ function InstalledTab({
1472
1980
  const updatable = (row) => UPDATABLE.has(row.status);
1473
1981
  function statusTag(row) {
1474
1982
  if (row.status === "unknown") return null;
1983
+ if (row.status === "no-upstream") {
1984
+ return row.source ? h("span", { className: "dshsc-tag", title: t("status.noUpstreamHint") }, t("status.noUpstream")) : null;
1985
+ }
1475
1986
  const map = {
1476
1987
  current: ["status.current", "ok", true],
1477
1988
  "update-available": ["status.update", "warn"],
@@ -1550,7 +2061,13 @@ function InstalledTab({
1550
2061
  h(
1551
2062
  "div",
1552
2063
  { className: "dshsc-namerow" },
1553
- h("span", { className: "dshsc-name", title: row.name }, row.name),
2064
+ h("button", {
2065
+ type: "button",
2066
+ className: "dshsc-name dshsc-namebtn",
2067
+ title: row.name,
2068
+ "aria-haspopup": "dialog",
2069
+ onClick: () => actions.onView(row)
2070
+ }, row.name),
1554
2071
  statusTag(row),
1555
2072
  off ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null
1556
2073
  ),
@@ -1572,6 +2089,13 @@ function InstalledTab({
1572
2089
  onClick: () => void actions.onToggleMode(row),
1573
2090
  title: tracked ? row.mode === "on" ? t("mode.off") : t("mode.on") : t("adopt.switchHint")
1574
2091
  }, h("span", { className: "dshsc-knob" })),
2092
+ h("button", {
2093
+ className: "dshsc-btn ghost dshsc-view",
2094
+ disabled: busy !== null,
2095
+ "aria-haspopup": "dialog",
2096
+ title: t("action.view"),
2097
+ onClick: () => actions.onView(row)
2098
+ }, h(EyeIcon, { size: 12 }), t("action.view")),
1575
2099
  row.status === "unmanaged" ? h("button", {
1576
2100
  className: "dshsc-btn ghost",
1577
2101
  disabled: !writable || busy !== null,
@@ -2315,19 +2839,1017 @@ ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
2315
2839
  return null;
2316
2840
  }
2317
2841
 
2842
+ // client/views/skill-reader.js
2843
+ var import_react4 = __toESM(require("react"), 1);
2844
+
2845
+ // client/markdown.js
2846
+ function classifyHref(href) {
2847
+ const value = String(href ?? "").trim();
2848
+ if (!value) return { kind: "unsafe", href: value };
2849
+ if (/[\u0000-\u001f\u007f]/.test(value)) return { kind: "unsafe", href: value };
2850
+ if (value.startsWith("#")) return { kind: "anchor", href: value.slice(1) };
2851
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value);
2852
+ if (scheme) {
2853
+ const name = scheme[1].toLowerCase();
2854
+ if (name === "http" || name === "https" || name === "mailto") return { kind: "external", href: value };
2855
+ return { kind: "unsafe", href: value };
2856
+ }
2857
+ if (value.startsWith("//")) return { kind: "unsafe", href: value };
2858
+ return { kind: "relative", href: value };
2859
+ }
2860
+ function slugify(text) {
2861
+ return String(text ?? "").trim().toLowerCase().replace(/[`*_~]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2862
+ }
2863
+ function stripFrontmatter(text) {
2864
+ const source = String(text ?? "");
2865
+ const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(source);
2866
+ if (!match) return { frontmatter: null, body: source };
2867
+ return { frontmatter: match[1], body: source.slice(match[0].length) };
2868
+ }
2869
+ var ESCAPABLE = /* @__PURE__ */ new Set(["\\", "`", "*", "_", "[", "]", "(", ")", "#", "+", "-", ".", "!", "|", ">", "~"]);
2870
+ function matchBracketLink(text, start) {
2871
+ const close = text.indexOf("]", start + 1);
2872
+ if (close < 0) return null;
2873
+ if (text[close + 1] !== "(") return null;
2874
+ let depth = 0;
2875
+ let index = close + 1;
2876
+ for (; index < text.length; index += 1) {
2877
+ if (text[index] === "(") depth += 1;
2878
+ else if (text[index] === ")") {
2879
+ depth -= 1;
2880
+ if (depth === 0) break;
2881
+ }
2882
+ }
2883
+ if (index >= text.length) return null;
2884
+ const label = text.slice(start + 1, close);
2885
+ const rawTarget = text.slice(close + 2, index).trim();
2886
+ const href = rawTarget.split(/\s+/)[0] || "";
2887
+ return { label, href, end: index + 1 };
2888
+ }
2889
+ function parseInline(text) {
2890
+ const source = String(text ?? "");
2891
+ const nodes = [];
2892
+ let buffer = "";
2893
+ const flush = () => {
2894
+ if (buffer) {
2895
+ nodes.push({ type: "text", value: buffer });
2896
+ buffer = "";
2897
+ }
2898
+ };
2899
+ let index = 0;
2900
+ while (index < source.length) {
2901
+ const char = source[index];
2902
+ if (char === "\\" && ESCAPABLE.has(source[index + 1])) {
2903
+ buffer += source[index + 1];
2904
+ index += 2;
2905
+ continue;
2906
+ }
2907
+ if (char === "`") {
2908
+ const end = source.indexOf("`", index + 1);
2909
+ if (end > index + 1) {
2910
+ flush();
2911
+ nodes.push({ type: "code", value: source.slice(index + 1, end) });
2912
+ index = end + 1;
2913
+ continue;
2914
+ }
2915
+ }
2916
+ if (char === "!" && source[index + 1] === "[") {
2917
+ const match = matchBracketLink(source, index + 1);
2918
+ if (match) {
2919
+ flush();
2920
+ const target = classifyHref(match.href);
2921
+ nodes.push({ type: "image", alt: match.label, src: match.href, kind: target.kind });
2922
+ index = match.end;
2923
+ continue;
2924
+ }
2925
+ }
2926
+ if (char === "[") {
2927
+ const match = matchBracketLink(source, index);
2928
+ if (match) {
2929
+ flush();
2930
+ const target = classifyHref(match.href);
2931
+ nodes.push({
2932
+ type: "link",
2933
+ href: target.href,
2934
+ kind: target.kind,
2935
+ children: parseInline(match.label)
2936
+ });
2937
+ index = match.end;
2938
+ continue;
2939
+ }
2940
+ }
2941
+ if (char === "*" || char === "_") {
2942
+ const doubled = source[index + 1] === char;
2943
+ const marker = doubled ? char + char : char;
2944
+ const end = source.indexOf(marker, index + marker.length);
2945
+ if (end > index + marker.length) {
2946
+ flush();
2947
+ nodes.push({
2948
+ type: doubled ? "strong" : "em",
2949
+ children: parseInline(source.slice(index + marker.length, end))
2950
+ });
2951
+ index = end + marker.length;
2952
+ continue;
2953
+ }
2954
+ }
2955
+ buffer += char;
2956
+ index += 1;
2957
+ }
2958
+ flush();
2959
+ return nodes;
2960
+ }
2961
+ function inlineToText(nodes) {
2962
+ if (!Array.isArray(nodes)) return String(nodes ?? "");
2963
+ return nodes.map((node) => {
2964
+ if (node.type === "text" || node.type === "code") return node.value;
2965
+ if (node.type === "image") return node.alt || "";
2966
+ return inlineToText(node.children);
2967
+ }).join("");
2968
+ }
2969
+ var FENCE = /^(\s*)(`{3,}|~{3,})\s*([^`]*)$/;
2970
+ var HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
2971
+ var HR = /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/;
2972
+ var LIST_ITEM = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
2973
+ var QUOTE = /^\s{0,3}>\s?(.*)$/;
2974
+ var TABLE_SEPARATOR = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
2975
+ function splitRow(line) {
2976
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
2977
+ return trimmed.split("|").map((cell) => cell.trim());
2978
+ }
2979
+ function alignOf(cell) {
2980
+ const value = cell.trim();
2981
+ if (/^:-+:$/.test(value)) return "center";
2982
+ if (/^:-+$/.test(value)) return "left";
2983
+ if (/^-+:$/.test(value)) return "right";
2984
+ return null;
2985
+ }
2986
+ function parseMarkdown(text) {
2987
+ const lines = String(text ?? "").replace(/\r\n?/g, "\n").split("\n");
2988
+ const blocks = [];
2989
+ const headings = [];
2990
+ const usedIds = /* @__PURE__ */ new Map();
2991
+ const headingId = (plain) => {
2992
+ const base = slugify(plain) || "section";
2993
+ const seen = usedIds.get(base) || 0;
2994
+ usedIds.set(base, seen + 1);
2995
+ return seen === 0 ? base : `${base}-${seen + 1}`;
2996
+ };
2997
+ const isBlockStart = (line) => FENCE.test(line) || HEADING.test(line) || HR.test(line) || LIST_ITEM.test(line) || QUOTE.test(line) || line.includes("|");
2998
+ let index = 0;
2999
+ while (index < lines.length) {
3000
+ const line = lines[index];
3001
+ if (!line.trim()) {
3002
+ index += 1;
3003
+ continue;
3004
+ }
3005
+ const fence = FENCE.exec(line);
3006
+ if (fence) {
3007
+ const marker = fence[2];
3008
+ const info = fence[3].trim();
3009
+ const body = [];
3010
+ index += 1;
3011
+ while (index < lines.length) {
3012
+ const current = lines[index];
3013
+ if (current.trim().startsWith(marker[0].repeat(marker.length)) && current.trim().replace(/[`~]/g, "") === "") {
3014
+ index += 1;
3015
+ break;
3016
+ }
3017
+ body.push(current);
3018
+ index += 1;
3019
+ }
3020
+ blocks.push({ type: "code", lang: info.split(/\s+/)[0] || "", text: body.join("\n") });
3021
+ continue;
3022
+ }
3023
+ const heading = HEADING.exec(line);
3024
+ if (heading) {
3025
+ const level = heading[1].length;
3026
+ const inlines = parseInline(heading[2]);
3027
+ const plain = inlineToText(inlines).trim();
3028
+ const id = headingId(plain);
3029
+ blocks.push({ type: "heading", level, id, inlines, text: plain });
3030
+ headings.push({ id, level, text: plain });
3031
+ index += 1;
3032
+ continue;
3033
+ }
3034
+ if (HR.test(line)) {
3035
+ blocks.push({ type: "hr" });
3036
+ index += 1;
3037
+ continue;
3038
+ }
3039
+ if (QUOTE.test(line)) {
3040
+ const quoted = [];
3041
+ while (index < lines.length && QUOTE.test(lines[index])) {
3042
+ quoted.push(QUOTE.exec(lines[index])[1]);
3043
+ index += 1;
3044
+ }
3045
+ blocks.push({ type: "quote", blocks: parseMarkdown(quoted.join("\n")).blocks });
3046
+ continue;
3047
+ }
3048
+ if (line.includes("|") && index + 1 < lines.length && TABLE_SEPARATOR.test(lines[index + 1])) {
3049
+ const header = splitRow(line).map((cell) => parseInline(cell));
3050
+ const align = splitRow(lines[index + 1]).map(alignOf);
3051
+ index += 2;
3052
+ const rows = [];
3053
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
3054
+ rows.push(splitRow(lines[index]).map((cell) => parseInline(cell)));
3055
+ index += 1;
3056
+ }
3057
+ blocks.push({ type: "table", align, header, rows });
3058
+ continue;
3059
+ }
3060
+ if (LIST_ITEM.test(line)) {
3061
+ const first = LIST_ITEM.exec(line);
3062
+ const ordered = /\d/.test(first[2]);
3063
+ const items = [];
3064
+ while (index < lines.length) {
3065
+ const match = LIST_ITEM.exec(lines[index]);
3066
+ if (!match) break;
3067
+ const indent = match[1].replace(/\t/g, " ").length;
3068
+ if (indent > 3) break;
3069
+ const content = match[3];
3070
+ let checked = null;
3071
+ const task = /^\[([ xX])\]\s*(.*)$/.exec(content);
3072
+ const text2 = task ? task[2] : content;
3073
+ if (task) checked = task[1].toLowerCase() === "x";
3074
+ index += 1;
3075
+ const nested = [];
3076
+ while (index < lines.length) {
3077
+ const child = LIST_ITEM.exec(lines[index]);
3078
+ if (!child) break;
3079
+ const childIndent = child[1].replace(/\t/g, " ").length;
3080
+ if (childIndent === 0) break;
3081
+ nested.push(lines[index]);
3082
+ index += 1;
3083
+ }
3084
+ while (index < lines.length && lines[index].trim() && !LIST_ITEM.test(lines[index]) && !isBlockStart(lines[index])) {
3085
+ index += 1;
3086
+ }
3087
+ const item = { inlines: parseInline(text2), checked };
3088
+ if (nested.length > 0) {
3089
+ const sub = parseMarkdown(nested.map((row) => row.replace(/^\s{1,4}/, "")).join("\n")).blocks;
3090
+ item.children = sub;
3091
+ }
3092
+ items.push(item);
3093
+ }
3094
+ blocks.push({ type: "list", ordered, items });
3095
+ continue;
3096
+ }
3097
+ const paragraph = [line];
3098
+ index += 1;
3099
+ while (index < lines.length && lines[index].trim() && !isBlockStart(lines[index])) {
3100
+ paragraph.push(lines[index]);
3101
+ index += 1;
3102
+ }
3103
+ blocks.push({ type: "paragraph", inlines: parseInline(paragraph.map((row) => row.trim()).join(" ")) });
3104
+ }
3105
+ return { blocks, headings };
3106
+ }
3107
+ function resolveRelativeHref(currentDir, href) {
3108
+ const target = classifyHref(href);
3109
+ if (target.kind === "anchor") return { relPath: "", anchor: target.href };
3110
+ const raw = String(target.href || "").split("#")[0].split("?")[0];
3111
+ const base = String(currentDir || "").replace(/^\/+|\/+$/g, "");
3112
+ if (!raw) return { relPath: "", anchor: target.kind === "anchor" ? target.href : "" };
3113
+ const segments = `${base ? `${base}/` : ""}${raw}`.split("/");
3114
+ const out = [];
3115
+ for (const segment of segments) {
3116
+ if (!segment || segment === ".") continue;
3117
+ if (segment === "..") {
3118
+ out.pop();
3119
+ continue;
3120
+ }
3121
+ out.push(segment);
3122
+ }
3123
+ return { relPath: out.join("/"), anchor: target.kind === "anchor" ? target.href : "" };
3124
+ }
3125
+
3126
+ // client/views/skill-reader.js
3127
+ var PARSE_CACHE = /* @__PURE__ */ new Map();
3128
+ function parseCached(text) {
3129
+ const key = String(text ?? "");
3130
+ if (PARSE_CACHE.has(key)) return PARSE_CACHE.get(key);
3131
+ const parsed = parseMarkdown(stripFrontmatter(key).body);
3132
+ PARSE_CACHE.set(key, parsed);
3133
+ if (PARSE_CACHE.size > 8) {
3134
+ const oldest = PARSE_CACHE.keys().next().value;
3135
+ PARSE_CACHE.delete(oldest);
3136
+ }
3137
+ return parsed;
3138
+ }
3139
+ function formatBytes(bytes) {
3140
+ const value = Number(bytes) || 0;
3141
+ if (value < 1024) return `${value} B`;
3142
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3143
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3144
+ }
3145
+ function formatWhen(iso) {
3146
+ if (!iso) return null;
3147
+ const date = new Date(iso);
3148
+ if (Number.isNaN(date.getTime())) return null;
3149
+ const pad = (value) => String(value).padStart(2, "0");
3150
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3151
+ }
3152
+ var MARKDOWN = /\.(md|markdown)$/i;
3153
+ function isMarkdownPath(relPath) {
3154
+ return MARKDOWN.test(String(relPath || ""));
3155
+ }
3156
+ function nearestScroller(node) {
3157
+ let current = node?.parentElement || null;
3158
+ while (current) {
3159
+ if (typeof getComputedStyle === "function") {
3160
+ const style = getComputedStyle(current);
3161
+ if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight + 4) {
3162
+ return current;
3163
+ }
3164
+ }
3165
+ current = current.parentElement || null;
3166
+ }
3167
+ return typeof document !== "undefined" ? document.scrollingElement || null : null;
3168
+ }
3169
+ function scrollHost(node) {
3170
+ return node && nearestScroller(node) || node;
3171
+ }
3172
+ var STATUS_KEY = {
3173
+ current: "status.current",
3174
+ "update-available": "status.update",
3175
+ "locally-modified": "status.local",
3176
+ conflict: "status.conflict",
3177
+ "upstream-deleted": "status.deleted",
3178
+ "broken-link": "status.broken",
3179
+ unmanaged: "status.unmanaged"
3180
+ };
3181
+ function inlineNodes(nodes, { onOpenFile, onOpenAnchor, keyPrefix = "i" }) {
3182
+ if (!Array.isArray(nodes)) return null;
3183
+ return nodes.map((node, index) => {
3184
+ const key = `${keyPrefix}-${index}`;
3185
+ switch (node.type) {
3186
+ case "text":
3187
+ return node.value;
3188
+ case "code":
3189
+ return h("code", { className: "dshsc-md-inline", key }, node.value);
3190
+ case "strong":
3191
+ return h("strong", { key }, inlineNodes(node.children, { onOpenFile, onOpenAnchor, keyPrefix: key }));
3192
+ case "em":
3193
+ return h("em", { key }, inlineNodes(node.children, { onOpenFile, onOpenAnchor, keyPrefix: key }));
3194
+ case "link": {
3195
+ const children = inlineNodes(node.children, { onOpenFile, onOpenAnchor, keyPrefix: key });
3196
+ if (node.kind === "external") {
3197
+ return h("a", {
3198
+ className: "dshsc-md-link",
3199
+ key,
3200
+ href: node.href,
3201
+ target: "_blank",
3202
+ rel: "noopener noreferrer"
3203
+ }, children);
3204
+ }
3205
+ if (node.kind === "anchor") {
3206
+ return h("a", {
3207
+ className: "dshsc-md-link",
3208
+ key,
3209
+ href: `#${node.href}`,
3210
+ onClick: (event) => {
3211
+ event?.preventDefault?.();
3212
+ onOpenAnchor?.(node.href);
3213
+ }
3214
+ }, children);
3215
+ }
3216
+ if (node.kind === "relative") {
3217
+ return h("a", {
3218
+ className: "dshsc-md-link",
3219
+ key,
3220
+ href: "#",
3221
+ title: node.href,
3222
+ onClick: (event) => {
3223
+ event?.preventDefault?.();
3224
+ onOpenFile?.(node.href);
3225
+ }
3226
+ }, children);
3227
+ }
3228
+ return h("span", { className: "dshsc-md-nolink", key }, children);
3229
+ }
3230
+ case "image":
3231
+ return h(
3232
+ "span",
3233
+ { className: "dshsc-md-image", key, title: node.src },
3234
+ h("span", { className: "dshsc-md-image-tag" }, "IMG"),
3235
+ node.alt || node.src
3236
+ );
3237
+ default:
3238
+ return null;
3239
+ }
3240
+ });
3241
+ }
3242
+ function blockNodes(blocks, options) {
3243
+ return blocks.map((block, index) => {
3244
+ const key = `b-${index}`;
3245
+ switch (block.type) {
3246
+ case "heading": {
3247
+ const tag = `h${Math.min(block.level, 6)}`;
3248
+ return h(
3249
+ tag,
3250
+ { className: "dshsc-md-h", key, id: block.id, "data-md-id": block.id },
3251
+ inlineNodes(block.inlines, { ...options, keyPrefix: key })
3252
+ );
3253
+ }
3254
+ case "paragraph":
3255
+ return h(
3256
+ "p",
3257
+ { className: "dshsc-md-p", key },
3258
+ inlineNodes(block.inlines, { ...options, keyPrefix: key })
3259
+ );
3260
+ case "code":
3261
+ return h(
3262
+ "pre",
3263
+ { className: "dshsc-md-code", key, "data-lang": block.lang || void 0 },
3264
+ h("code", null, block.text)
3265
+ );
3266
+ case "hr":
3267
+ return h("hr", { className: "dshsc-md-hr", key });
3268
+ case "quote":
3269
+ return h("blockquote", { className: "dshsc-md-quote", key }, blockNodes(block.blocks, options));
3270
+ case "list": {
3271
+ const tag = block.ordered ? "ol" : "ul";
3272
+ return h(tag, { className: "dshsc-md-list", key }, block.items.map((item, itemIndex) => h(
3273
+ "li",
3274
+ {
3275
+ key: `${key}-${itemIndex}`,
3276
+ className: item.checked === null || item.checked === void 0 ? void 0 : "dshsc-md-task"
3277
+ },
3278
+ item.checked === null || item.checked === void 0 ? null : h("span", { className: `dshsc-md-check${item.checked ? " on" : ""}`, "aria-hidden": true }),
3279
+ inlineNodes(item.inlines, { ...options, keyPrefix: `${key}-${itemIndex}` }),
3280
+ Array.isArray(item.children) && item.children.length > 0 ? blockNodes(item.children, options) : null
3281
+ )));
3282
+ }
3283
+ case "table":
3284
+ return h(
3285
+ "div",
3286
+ { className: "dshsc-md-table", key },
3287
+ h(
3288
+ "table",
3289
+ null,
3290
+ h("thead", null, h("tr", null, block.header.map((cell, cellIndex) => h("th", {
3291
+ key: `h-${cellIndex}`,
3292
+ style: { textAlign: block.align?.[cellIndex] || "left" }
3293
+ }, inlineNodes(cell, { ...options, keyPrefix: `${key}-h-${cellIndex}` }))))),
3294
+ h("tbody", null, block.rows.map((row, rowIndex) => h(
3295
+ "tr",
3296
+ { key: `r-${rowIndex}` },
3297
+ row.map((cell, cellIndex) => h("td", {
3298
+ key: `c-${cellIndex}`,
3299
+ style: { textAlign: block.align?.[cellIndex] || "left" }
3300
+ }, inlineNodes(cell, { ...options, keyPrefix: `${key}-${rowIndex}-${cellIndex}` })))
3301
+ )))
3302
+ )
3303
+ );
3304
+ default:
3305
+ return null;
3306
+ }
3307
+ });
3308
+ }
3309
+ function Markdown({ text, onOpenFile, onOpenAnchor }) {
3310
+ const { blocks } = parseCached(text);
3311
+ return h("div", { className: "dshsc-md" }, blockNodes(blocks, { onOpenFile, onOpenAnchor }));
3312
+ }
3313
+ function TabButton({ id, label, count, active, onSelect }) {
3314
+ return h("button", {
3315
+ type: "button",
3316
+ role: "tab",
3317
+ id: `dshsc-tab-${id}`,
3318
+ "aria-selected": String(active),
3319
+ "aria-controls": `dshsc-panel-${id}`,
3320
+ tabIndex: active ? 0 : -1,
3321
+ className: `dshsc-rtab${active ? " active" : ""}`,
3322
+ onClick: () => onSelect(id)
3323
+ }, label, count === null || count === void 0 ? null : h("i", { className: "n" }, count));
3324
+ }
3325
+ function InfoSection({ t, title, rows }) {
3326
+ return h(
3327
+ "section",
3328
+ { className: "dshsc-info-sec" },
3329
+ h("h3", null, title),
3330
+ h("dl", null, rows.map((row, index) => [
3331
+ h("dt", { key: `t-${index}` }, t(row[0])),
3332
+ h("dd", { key: `d-${index}`, className: row[2] === "mono" ? "mono" : void 0 }, row[1])
3333
+ ]))
3334
+ );
3335
+ }
3336
+ function SkillReader({
3337
+ t,
3338
+ variant = "drawer",
3339
+ row,
3340
+ writable,
3341
+ busy,
3342
+ readOnly,
3343
+ version,
3344
+ initialTab = "doc",
3345
+ initialFile = null,
3346
+ onClose,
3347
+ onOpenPage,
3348
+ onUpdate,
3349
+ onLinks,
3350
+ onToast
3351
+ }) {
3352
+ const { state, file, loadFile, retry } = useSkillReader(row);
3353
+ const [tab, setTab] = import_react4.default.useState(initialTab);
3354
+ const [format, setFormat] = import_react4.default.useState("rendered");
3355
+ const [docOverride, setDocOverride] = import_react4.default.useState(null);
3356
+ const [fileFormat, setFileFormat] = import_react4.default.useState("rendered");
3357
+ const [tocActive, setTocActive] = import_react4.default.useState(null);
3358
+ const bodyRef = import_react4.default.useRef(null);
3359
+ const dialogRef = import_react4.default.useRef(null);
3360
+ const scrollRef = import_react4.default.useRef(0);
3361
+ const name = row?.name || "";
3362
+ const tree = state.tree;
3363
+ const files = tree?.files || [];
3364
+ const fileCount = files.filter((entry) => entry.kind === "file").length;
3365
+ const bodyText = docOverride ? docOverride.content : state.body?.content || "";
3366
+ const description = row?.description || state.body?.meta?.description || "";
3367
+ const headings = tab === "doc" && bodyText ? parseCached(bodyText).headings : [];
3368
+ import_react4.default.useEffect(() => {
3369
+ setDocOverride(null);
3370
+ scrollRef.current = 0;
3371
+ }, [name]);
3372
+ import_react4.default.useEffect(() => {
3373
+ if (!initialFile) return;
3374
+ setTab("files");
3375
+ void loadFile(initialFile);
3376
+ }, [initialFile, loadFile]);
3377
+ import_react4.default.useEffect(() => {
3378
+ const node = bodyRef.current;
3379
+ if (node && scrollRef.current) node.scrollTop = scrollRef.current;
3380
+ });
3381
+ import_react4.default.useEffect(() => {
3382
+ if (variant !== "drawer" || typeof document === "undefined") return void 0;
3383
+ const previous = document.activeElement;
3384
+ const node = dialogRef.current;
3385
+ node?.focus?.();
3386
+ const onKeyDown = (event) => {
3387
+ if (event?.key === "Escape") {
3388
+ event.stopPropagation?.();
3389
+ onClose?.();
3390
+ return;
3391
+ }
3392
+ if (event?.key !== "Tab" || !node?.querySelectorAll) return;
3393
+ const focusables = Array.from(
3394
+ node.querySelectorAll('a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])')
3395
+ ).filter((item) => item.offsetParent !== null || item === document.activeElement);
3396
+ if (focusables.length === 0) return;
3397
+ const first = focusables[0];
3398
+ const last = focusables[focusables.length - 1];
3399
+ if (event.shiftKey && document.activeElement === first) {
3400
+ event.preventDefault?.();
3401
+ last.focus?.();
3402
+ } else if (!event.shiftKey && document.activeElement === last) {
3403
+ event.preventDefault?.();
3404
+ first.focus?.();
3405
+ }
3406
+ };
3407
+ document.addEventListener("keydown", onKeyDown);
3408
+ return () => {
3409
+ document.removeEventListener("keydown", onKeyDown);
3410
+ previous?.focus?.();
3411
+ };
3412
+ }, [variant, onClose]);
3413
+ import_react4.default.useEffect(() => {
3414
+ if (variant !== "page" || typeof document === "undefined") return;
3415
+ const node = document.getElementById("dshsc-reader-name");
3416
+ node?.focus?.();
3417
+ }, [variant, name]);
3418
+ import_react4.default.useEffect(() => {
3419
+ if (variant !== "page" || typeof window === "undefined") return void 0;
3420
+ const node = bodyRef.current;
3421
+ if (!node?.querySelectorAll) return void 0;
3422
+ const scroller = scrollHost(node);
3423
+ const sync = () => {
3424
+ const line = (scroller?.getBoundingClientRect?.().top ?? 0) + 96;
3425
+ let current = null;
3426
+ for (const element of Array.from(node.querySelectorAll("[data-md-id]"))) {
3427
+ const top = element.getBoundingClientRect?.().top ?? 0;
3428
+ if (top <= line) current = element.getAttribute("data-md-id");
3429
+ }
3430
+ setTocActive(current || headings[0]?.id || null);
3431
+ };
3432
+ sync();
3433
+ scroller?.addEventListener?.("scroll", sync, { passive: true });
3434
+ return () => scroller?.removeEventListener?.("scroll", sync);
3435
+ }, [variant, headings.length, tab, bodyText]);
3436
+ function scrollToAnchor(id) {
3437
+ const node = bodyRef.current;
3438
+ if (!node?.querySelectorAll) return;
3439
+ const target = Array.from(node.querySelectorAll("[data-md-id]")).find((element) => element.getAttribute("data-md-id") === id);
3440
+ if (!target) return;
3441
+ const scroller = scrollHost(node);
3442
+ const offset = target.getBoundingClientRect().top - (scroller?.getBoundingClientRect?.().top ?? 0);
3443
+ if (scroller) scroller.scrollTop = Math.max(0, (scroller.scrollTop || 0) + offset - 12);
3444
+ setTocActive(id);
3445
+ }
3446
+ function openLinkedFile(href, fromPath) {
3447
+ const source = fromPath || docOverride?.relPath || "";
3448
+ const base = source.split("/").slice(0, -1).join("/");
3449
+ const resolved = resolveRelativeHref(base, href);
3450
+ if (!resolved.relPath) return;
3451
+ setTab("files");
3452
+ void loadFile(resolved.relPath);
3453
+ }
3454
+ async function copyTextWithToast(text, okKey) {
3455
+ let ok = false;
3456
+ try {
3457
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
3458
+ await navigator.clipboard.writeText(text);
3459
+ ok = true;
3460
+ }
3461
+ } catch {
3462
+ ok = false;
3463
+ }
3464
+ onToast?.(ok ? { kind: "ok", text: t(okKey) } : { kind: "err", text: t("viewer.copyFailed") });
3465
+ }
3466
+ function selectTab(next) {
3467
+ if (next === tab) return;
3468
+ if (bodyRef.current) scrollRef.current = 0;
3469
+ setTab(next);
3470
+ }
3471
+ const header = h(
3472
+ "header",
3473
+ { className: "dshsc-reader-head" },
3474
+ h(
3475
+ "div",
3476
+ { className: "dshsc-reader-title" },
3477
+ h("h2", { id: "dshsc-reader-name", tabIndex: -1 }, name || t("viewer.title")),
3478
+ row?.status && STATUS_KEY[row.status] ? h("span", { className: `dshsc-tag ${row.status === "current" ? "ok" : "warn"}` }, t(STATUS_KEY[row.status])) : null,
3479
+ row?.mode === "off" ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null,
3480
+ variant === "drawer" ? h("button", {
3481
+ type: "button",
3482
+ className: "dshsc-reader-close",
3483
+ "aria-label": t("viewer.close"),
3484
+ title: t("viewer.close"),
3485
+ onClick: () => onClose?.()
3486
+ }, "×") : null
3487
+ ),
3488
+ description ? h("p", { className: "dshsc-reader-desc" }, description) : null,
3489
+ h(
3490
+ "div",
3491
+ { className: "dshsc-reader-meta" },
3492
+ h(
3493
+ "span",
3494
+ null,
3495
+ `${t("meta.from")} `,
3496
+ h("span", { className: "dshsc-code" }, row?.source ? `${row.source}${row.commit ? `#${short(row.commit)}` : row.ref ? `#${row.ref}` : ""}` : t("meta.noUpstream"))
3497
+ ),
3498
+ tree ? h("span", null, t("viewer.fileCount").replace("{n}", String(fileCount))) : null,
3499
+ row?.updatedAt ? h("span", null, formatWhen(row.updatedAt)) : null
3500
+ )
3501
+ );
3502
+ const tabsBar = h(
3503
+ "div",
3504
+ { className: "dshsc-reader-tabs" },
3505
+ h(
3506
+ "div",
3507
+ { className: "dshsc-rtabs", role: "tablist", "aria-label": t("viewer.title") },
3508
+ h(TabButton, { id: "doc", label: t("viewer.tab.doc"), active: tab === "doc", onSelect: selectTab }),
3509
+ h(TabButton, { id: "files", label: t("viewer.tab.files"), count: tree ? fileCount : null, active: tab === "files", onSelect: selectTab }),
3510
+ h(TabButton, { id: "info", label: t("viewer.tab.info"), active: tab === "info", onSelect: selectTab })
3511
+ ),
3512
+ tab === "doc" && !docOverride ? h(
3513
+ "div",
3514
+ { className: "dshsc-segment dshsc-reader-seg", role: "group", "aria-label": t("viewer.rendered") },
3515
+ h("button", {
3516
+ type: "button",
3517
+ className: "dshsc-btn",
3518
+ "aria-pressed": String(format === "rendered"),
3519
+ onClick: () => setFormat("rendered")
3520
+ }, t("viewer.rendered")),
3521
+ h("button", {
3522
+ type: "button",
3523
+ className: "dshsc-btn",
3524
+ "aria-pressed": String(format === "raw"),
3525
+ onClick: () => setFormat("raw")
3526
+ }, t("viewer.raw"))
3527
+ ) : null
3528
+ );
3529
+ function docPanel() {
3530
+ if (state.loading) {
3531
+ return h(
3532
+ "div",
3533
+ { className: "dshsc-reader-skeleton" },
3534
+ h("span", null, t("viewer.loading")),
3535
+ h("i"),
3536
+ h("i"),
3537
+ h("i")
3538
+ );
3539
+ }
3540
+ if (!bodyText) {
3541
+ const error = state.bodyError;
3542
+ const missing = error?.code === "error.skills.no-skill-md";
3543
+ return h(
3544
+ "div",
3545
+ { className: "dshsc-reader-placeholder" },
3546
+ h("b", null, missing ? t("viewer.noSkill") : error?.message || t("viewer.noSkill")),
3547
+ missing ? h("span", null, t("viewer.noSkillHint")) : null,
3548
+ error && !missing ? h("button", { type: "button", className: "dshsc-btn", onClick: retry }, t("viewer.retry")) : null
3549
+ );
3550
+ }
3551
+ if (docOverride) {
3552
+ return h(
3553
+ "div",
3554
+ { className: "dshsc-reader-doc" },
3555
+ h(
3556
+ "div",
3557
+ { className: "dshsc-reader-filebar" },
3558
+ h("span", { className: "mono" }, docOverride.relPath),
3559
+ h("button", {
3560
+ type: "button",
3561
+ className: "dshsc-btn",
3562
+ onClick: () => setDocOverride(null)
3563
+ }, t("viewer.backToBody"))
3564
+ ),
3565
+ h(Markdown, { text: docOverride.content, onOpenFile: openLinkedFile, onOpenAnchor: scrollToAnchor })
3566
+ );
3567
+ }
3568
+ const truncated = Boolean(state.body?.truncated);
3569
+ return h(
3570
+ "div",
3571
+ { className: "dshsc-reader-doc" },
3572
+ truncated ? h("div", { className: "dshsc-banner warn" }, t("viewer.tooLarge").replace("{size}", formatBytes(256 * 1024))) : null,
3573
+ format === "raw" ? h("pre", { className: "dshsc-reader-raw" }, h("code", null, bodyText)) : h(Markdown, { text: bodyText, onOpenFile: openLinkedFile, onOpenAnchor: scrollToAnchor })
3574
+ );
3575
+ }
3576
+ function filesPanel() {
3577
+ if (state.treeError) {
3578
+ return h(
3579
+ "div",
3580
+ { className: "dshsc-reader-placeholder" },
3581
+ h("b", null, state.treeError.message),
3582
+ h("button", { type: "button", className: "dshsc-btn", onClick: retry }, t("viewer.retry"))
3583
+ );
3584
+ }
3585
+ if (!tree) return h("div", { className: "dshsc-reader-skeleton" }, h("i"), h("i"));
3586
+ if (files.length === 0) {
3587
+ return h("div", { className: "dshsc-reader-placeholder" }, h("b", null, t("viewer.emptyFiles")));
3588
+ }
3589
+ const active = file.relPath;
3590
+ const activeEntry = files.find((entry) => entry.kind === "file" && entry.path === active) || null;
3591
+ const renderable = Boolean(active) && isMarkdownPath(active) && !file.error && !file.loading;
3592
+ const preview = file.loading ? h("div", { className: "dshsc-reader-skeleton" }, h("i"), h("i"), h("i")) : file.error ? h(
3593
+ "div",
3594
+ { className: "dshsc-reader-placeholder" },
3595
+ h("b", null, file.error.code === "error.skills.binary" ? t("viewer.binary") : file.error.message),
3596
+ h(
3597
+ "span",
3598
+ { className: "mono" },
3599
+ [active, activeEntry ? formatBytes(activeEntry.size) : null].filter(Boolean).join(" · ")
3600
+ ),
3601
+ file.error.code === "error.skills.binary" ? h("span", null, t("viewer.binaryHint")) : null
3602
+ ) : renderable && fileFormat === "rendered" ? h(
3603
+ "div",
3604
+ { className: "dshsc-reader-preview-doc" },
3605
+ h(Markdown, {
3606
+ text: file.content,
3607
+ onOpenFile: (href) => openLinkedFile(href, file.relPath || active),
3608
+ onOpenAnchor: scrollToAnchor
3609
+ })
3610
+ ) : h(
3611
+ "pre",
3612
+ { className: "dshsc-reader-raw" },
3613
+ h("code", null, file.content + (file.truncated ? `
3614
+
3615
+ … ${t("viewer.tooLarge").replace("{size}", formatBytes(256 * 1024))}` : ""))
3616
+ );
3617
+ return h(
3618
+ "div",
3619
+ { className: "dshsc-reader-files" },
3620
+ h(
3621
+ "div",
3622
+ { className: "dshsc-reader-tree" },
3623
+ h(
3624
+ "div",
3625
+ { className: "dshsc-reader-tree-head" },
3626
+ h("span", null, name),
3627
+ h("span", { className: "mono" }, tree.kind === "file" ? tree.body : `~/.agents/skills/${name}`)
3628
+ ),
3629
+ files.map((entry) => {
3630
+ if (entry.kind === "dir") {
3631
+ const count = files.filter((item) => item.kind === "file" && item.path.startsWith(`${entry.path}/`)).length;
3632
+ return h(
3633
+ "div",
3634
+ { className: "dshsc-reader-tdir", key: `d:${entry.path}` },
3635
+ h(FolderIcon, null),
3636
+ h("span", { className: "mono" }, `${entry.path}/`),
3637
+ h("span", { className: "n" }, String(count))
3638
+ );
3639
+ }
3640
+ const depth = entry.path.split("/").length - 1;
3641
+ return h(
3642
+ "button",
3643
+ {
3644
+ type: "button",
3645
+ key: `f:${entry.path}`,
3646
+ className: `dshsc-reader-titem${active === entry.path ? " on" : ""}`,
3647
+ style: { paddingLeft: 8 + depth * 12 },
3648
+ title: entry.path,
3649
+ onClick: () => {
3650
+ setFileFormat("rendered");
3651
+ void loadFile(entry.path);
3652
+ }
3653
+ },
3654
+ h(FileIcon, null),
3655
+ h("span", { className: "nname" }, entry.path.split("/").pop()),
3656
+ h("span", { className: "nsize" }, formatBytes(entry.size))
3657
+ );
3658
+ })
3659
+ ),
3660
+ h(
3661
+ "div",
3662
+ { className: "dshsc-reader-preview" },
3663
+ h(
3664
+ "div",
3665
+ { className: "dshsc-reader-tree-head" },
3666
+ h("span", null, t("viewer.tab.files")),
3667
+ h("span", { className: "mono" }, active || "—"),
3668
+ // 预览的三个动作放在一起,靠右成一排,而不是一个贴路径、一个飘到最右
3669
+ h(
3670
+ "div",
3671
+ { className: "dshsc-reader-tools" },
3672
+ renderable ? h(
3673
+ "div",
3674
+ { className: "dshsc-segment dshsc-reader-seg", role: "group", "aria-label": t("viewer.rendered") },
3675
+ h("button", {
3676
+ type: "button",
3677
+ className: "dshsc-btn",
3678
+ "aria-pressed": String(fileFormat === "rendered"),
3679
+ onClick: () => setFileFormat("rendered")
3680
+ }, t("viewer.rendered")),
3681
+ h("button", {
3682
+ type: "button",
3683
+ className: "dshsc-btn",
3684
+ "aria-pressed": String(fileFormat === "raw"),
3685
+ onClick: () => setFileFormat("raw")
3686
+ }, t("viewer.raw"))
3687
+ ) : null,
3688
+ active && !file.loading && !file.error && isMarkdownPath(active) ? h("button", {
3689
+ type: "button",
3690
+ className: "dshsc-btn",
3691
+ onClick: () => {
3692
+ setDocOverride({ relPath: file.relPath || active, content: file.content });
3693
+ setTab("doc");
3694
+ }
3695
+ }, t("viewer.openInDoc")) : null
3696
+ )
3697
+ ),
3698
+ preview
3699
+ )
3700
+ );
3701
+ }
3702
+ function infoPanel() {
3703
+ const agentLabels = (row?.links || []).filter((link) => !link.broken).map((link) => link.target === "dsh-global" ? "DSH" : link.label);
3704
+ const projects = row?.scopeProjects || [];
3705
+ const hash = row?.lockHash || row?.currentHash || null;
3706
+ const rows = [];
3707
+ rows.push(["viewer.info.name", h("span", { className: "mono" }, name), "mono"]);
3708
+ if (description) rows.push(["viewer.info.desc", description]);
3709
+ rows.push(["viewer.info.path", h("span", { className: "mono" }, row?.path || (tree?.root ?? t("viewer.info.none"))), "mono"]);
3710
+ const sourceRows = [
3711
+ ["viewer.info.repo", row?.source ? h("span", { className: "mono" }, row.source) : t("viewer.info.none"), "mono"],
3712
+ ["viewer.info.ref", row?.commit ? h("span", { className: "mono" }, short(row.commit)) : row?.ref || t("viewer.info.none"), "mono"],
3713
+ ["viewer.info.skillPath", row?.skillPath ? h("span", { className: "mono" }, row.skillPath) : t("viewer.info.none"), "mono"],
3714
+ ["viewer.info.hash", hash ? h("span", { className: "mono" }, hash) : t("viewer.info.none"), "mono"]
3715
+ ];
3716
+ const localRows = [
3717
+ ["viewer.info.installedAt", formatWhen(row?.installedAt) || t("viewer.info.none")],
3718
+ ["viewer.info.updatedAt", formatWhen(row?.updatedAt) || t("viewer.info.none")],
3719
+ ["viewer.info.size", tree ? `${formatBytes(tree.totalBytes)} · ${t("viewer.fileCount").replace("{n}", String(fileCount))}` : t("viewer.info.none")],
3720
+ ["viewer.info.localChange", row?.status === "locally-modified" || row?.status === "conflict" ? t("viewer.info.localChangeYes") : t("viewer.info.localChangeNone")]
3721
+ ];
3722
+ const linkRows = [
3723
+ ["viewer.info.agents", agentLabels.length > 0 ? agentLabels.join("、") : t("meta.unlinked")],
3724
+ ["viewer.info.scope", projects.length > 0 ? `${t("viewer.info.scopeProject")}:${projects.map((item) => item.split("/").pop()).join("、")}` : t("viewer.info.scopeGlobal")],
3725
+ ["viewer.info.mode", row?.mode === "off" ? t("status.off") : t("mode.on")],
3726
+ ["viewer.info.status", STATUS_KEY[row?.status] ? t(STATUS_KEY[row.status]) : t("status.unknown")]
3727
+ ];
3728
+ return h(
3729
+ "div",
3730
+ { className: "dshsc-reader-info" },
3731
+ h(InfoSection, { t, title: t("viewer.info.basic"), rows }),
3732
+ h(InfoSection, { t, title: t("viewer.info.source"), rows: sourceRows }),
3733
+ h(InfoSection, { t, title: t("viewer.info.local"), rows: localRows }),
3734
+ h(InfoSection, { t, title: t("viewer.info.links"), rows: linkRows })
3735
+ );
3736
+ }
3737
+ function panel() {
3738
+ if (tab === "files") return filesPanel();
3739
+ if (tab === "info") return infoPanel();
3740
+ return docPanel();
3741
+ }
3742
+ const body = h("div", {
3743
+ className: "dshsc-reader-body",
3744
+ ref: bodyRef,
3745
+ role: "tabpanel",
3746
+ id: `dshsc-panel-${tab}`,
3747
+ "aria-labelledby": `dshsc-tab-${tab}`,
3748
+ tabIndex: -1
3749
+ }, panel());
3750
+ const canUpdate = Boolean(writable) && Boolean(row) && !busy;
3751
+ const actionsFooter = h(
3752
+ "div",
3753
+ { className: "dshsc-reader-actions" },
3754
+ h("span", { className: "dshsc-reader-note" }, readOnly ? t("banner.readonly") : t("viewer.readonlyNote")),
3755
+ h("button", {
3756
+ type: "button",
3757
+ // 抽屉底栏空间紧,图标按钮;整页顶栏跟一排文字按钮并排,用文字更齐
3758
+ className: variant === "page" ? "dshsc-btn ghost" : "dshsc-btn ghost sq",
3759
+ title: t("viewer.copyBody"),
3760
+ "aria-label": t("viewer.copyBody"),
3761
+ disabled: !bodyText,
3762
+ onClick: () => void copyTextWithToast(bodyText, "viewer.copied")
3763
+ }, h(CopyIcon, null), variant === "page" ? t("viewer.copyBody") : null),
3764
+ h("button", {
3765
+ type: "button",
3766
+ className: "dshsc-btn ghost",
3767
+ title: row?.path || "",
3768
+ disabled: !row?.path,
3769
+ onClick: () => void copyTextWithToast(row?.path || "", "viewer.copied")
3770
+ }, t("viewer.copyPath")),
3771
+ variant === "drawer" && onOpenPage ? h("button", { type: "button", className: "dshsc-btn ghost", onClick: () => onOpenPage() }, t("viewer.openPage")) : null,
3772
+ onLinks ? h("button", { type: "button", className: "dshsc-btn", onClick: () => onLinks?.(row) }, t("action.links")) : null,
3773
+ onUpdate ? h("button", {
3774
+ type: "button",
3775
+ className: "dshsc-btn tint",
3776
+ disabled: !canUpdate,
3777
+ onClick: () => onUpdate?.(row)
3778
+ }, t("action.update")) : null
3779
+ );
3780
+ if (variant === "page") {
3781
+ return h(
3782
+ "div",
3783
+ { className: "dshsc-reader-page" },
3784
+ h(
3785
+ "div",
3786
+ { className: "dshsc-reader-topbar" },
3787
+ h(
3788
+ "button",
3789
+ { type: "button", className: "dshsc-btn ghost", "data-autofocus": true, onClick: () => onClose?.() },
3790
+ h(ChevronLeftIcon, null),
3791
+ t("viewer.back")
3792
+ ),
3793
+ h(
3794
+ "span",
3795
+ { className: "dshsc-reader-crumb" },
3796
+ h("span", null, t("tab.installed")),
3797
+ h("span", { className: "sep" }, "/"),
3798
+ h("span", { className: "mono" }, name)
3799
+ ),
3800
+ h("span", { className: "spacer" }),
3801
+ actionsFooter
3802
+ ),
3803
+ h("div", { className: "dshsc-reader-hero" }, header),
3804
+ h(
3805
+ "div",
3806
+ { className: "dshsc-reader-layout" },
3807
+ headings.length > 1 ? h(
3808
+ "nav",
3809
+ { className: "dshsc-reader-toc", "aria-label": t("viewer.toc") },
3810
+ h("div", { className: "dshsc-reader-toc-title" }, t("viewer.toc")),
3811
+ headings.map((heading) => h("button", {
3812
+ type: "button",
3813
+ key: heading.id,
3814
+ className: `dshsc-reader-tocitem l${heading.level}${tocActive === heading.id ? " on" : ""}`,
3815
+ onClick: () => scrollToAnchor(heading.id)
3816
+ }, heading.text))
3817
+ ) : null,
3818
+ h("div", { className: "dshsc-reader-col" }, tabsBar, body)
3819
+ )
3820
+ );
3821
+ }
3822
+ return portalize(h(
3823
+ "div",
3824
+ { className: "dshsc-drawer-wrap" },
3825
+ h("div", { className: "dshsc-drawer-scrim", onClick: () => onClose?.() }),
3826
+ h("aside", {
3827
+ className: "dshsc-drawer",
3828
+ role: "dialog",
3829
+ "aria-modal": "true",
3830
+ "aria-labelledby": "dshsc-reader-name",
3831
+ ref: dialogRef,
3832
+ tabIndex: -1
3833
+ }, header, tabsBar, body, actionsFooter)
3834
+ ));
3835
+ }
3836
+
2318
3837
  // client/section.js
2319
3838
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
2320
3839
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2321
3840
  const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
2322
- const [tab, setTab] = import_react4.default.useState("installed");
2323
- const [view, setView] = import_react4.default.useState("flat");
2324
- const [modal, setModal] = import_react4.default.useState(null);
2325
- const [expanded, setExpanded] = import_react4.default.useState({});
2326
- const [placementBusy, setPlacementBusy] = import_react4.default.useState(null);
2327
- const [placementOverride, setPlacementOverride] = import_react4.default.useState(null);
2328
- const [query, setQuery] = import_react4.default.useState("");
2329
- const [filter, setFilter] = import_react4.default.useState("all");
2330
- const [builtinOpen, setBuiltinOpen] = import_react4.default.useState(false);
3841
+ const [tab, setTab] = import_react5.default.useState("installed");
3842
+ const [view, setView] = import_react5.default.useState("flat");
3843
+ const [modal, setModal] = import_react5.default.useState(null);
3844
+ const [expanded, setExpanded] = import_react5.default.useState({});
3845
+ const [placementBusy, setPlacementBusy] = import_react5.default.useState(null);
3846
+ const [placementOverride, setPlacementOverride] = import_react5.default.useState(null);
3847
+ const [query, setQuery] = import_react5.default.useState("");
3848
+ const [viewer, setViewer] = import_react5.default.useState(null);
3849
+ const rootRef = import_react5.default.useRef(null);
3850
+ const scrollPos = import_react5.default.useRef(null);
3851
+ const [filter, setFilter] = import_react5.default.useState("all");
3852
+ const [builtinOpen, setBuiltinOpen] = import_react5.default.useState(false);
2331
3853
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
2332
3854
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
2333
3855
  const rows = data?.installs || [];
@@ -2348,12 +3870,55 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2348
3870
  const agentLabels = row.links.map((link) => link.target === "dsh-global" ? "DSH" : link.label);
2349
3871
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
2350
3872
  });
3873
+ import_react5.default.useEffect(() => {
3874
+ if (!viewer || !data) return;
3875
+ if (rows.some((row) => row.name === viewer.name)) return;
3876
+ setViewer(null);
3877
+ setToast({ kind: "err", text: t("viewer.gone") });
3878
+ }, [data, rows, t, viewer]);
2351
3879
  const autolinkTargets = (data?.agents?.detected || []).filter((agent) => agent.kind === "agent");
2352
3880
  const autoLinkSelection = data?.settings?.autoLinkTargets;
2353
3881
  const autoLinkAgents = Array.isArray(autoLinkSelection) ? autolinkTargets.filter((agent) => autoLinkSelection.includes(agent.target)) : autolinkTargets;
2354
3882
  const autoLinkDsh = (data?.agents?.all || []).find((agent) => agent.target === "dsh-global") || null;
2355
3883
  const autoLinkLabel = [t("links.dshGlobal"), ...autoLinkAgents.map((agent) => agent.label)].join("、");
2356
3884
  const placement = (placementOverride ?? data?.settings?.placement) === "sidebar" ? "sidebar" : "settings";
3885
+ function getScrollParent(node) {
3886
+ let current = node?.parentElement || null;
3887
+ while (current) {
3888
+ if (typeof getComputedStyle === "function") {
3889
+ const style = getComputedStyle(current);
3890
+ if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight) {
3891
+ return current;
3892
+ }
3893
+ }
3894
+ current = current.parentElement || null;
3895
+ }
3896
+ return typeof document !== "undefined" ? document.scrollingElement || null : null;
3897
+ }
3898
+ function openViewer(row, mode = "drawer") {
3899
+ if (!row) return;
3900
+ if (mode === "page") {
3901
+ scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3902
+ }
3903
+ setViewer({ name: row.name, mode });
3904
+ }
3905
+ function openPage() {
3906
+ setViewer((prev) => {
3907
+ if (!prev) return prev;
3908
+ scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3909
+ return { ...prev, mode: "page" };
3910
+ });
3911
+ }
3912
+ function closeViewer() {
3913
+ const restore = viewer?.mode === "page" ? scrollPos.current : null;
3914
+ setViewer(null);
3915
+ if (restore === null || typeof window === "undefined") return;
3916
+ const apply2 = () => {
3917
+ const scroller = getScrollParent(rootRef.current);
3918
+ if (scroller) scroller.scrollTop = restore;
3919
+ };
3920
+ if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(apply2);
3921
+ }
2357
3922
  async function changePlacement(next) {
2358
3923
  if (next === placement || placementBusy) return;
2359
3924
  setPlacementBusy(next);
@@ -2373,6 +3938,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2373
3938
  await run("settings", () => apiCall("POST", "/settings", { autoLinkTargets: targets }));
2374
3939
  }
2375
3940
  const installedActions = {
3941
+ onView: (row) => openViewer(row),
2376
3942
  onAdopt: actions.adopt,
2377
3943
  onToggleMode: actions.toggleMode,
2378
3944
  onUpdate: actions.requestUpdate,
@@ -2398,10 +3964,23 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2398
3964
  onInstall: actions.installFromSubscription,
2399
3965
  onAdd: actions.requestAddSubscription
2400
3966
  };
2401
- const root = h(
2402
- "div",
2403
- { className: "dshsc-root" },
2404
- h("style", null, CSS),
3967
+ const viewerRow = viewer ? rows.find((row) => row.name === viewer.name) || null : null;
3968
+ const readerNode = viewerRow ? h(SkillReader, {
3969
+ t,
3970
+ variant: viewer.mode,
3971
+ row: viewerRow,
3972
+ writable,
3973
+ busy,
3974
+ readOnly: Boolean(data?.readOnly),
3975
+ onClose: closeViewer,
3976
+ onOpenPage: viewer.mode === "drawer" ? openPage : null,
3977
+ onUpdate: actions.requestUpdate,
3978
+ onLinks: (row) => setModal({ type: "links", row }),
3979
+ onToast: (next) => setToast(next)
3980
+ }) : null;
3981
+ const listNode = h(
3982
+ import_react5.default.Fragment,
3983
+ null,
2405
3984
  h(Header, {
2406
3985
  t,
2407
3986
  page,
@@ -2420,7 +3999,8 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2420
3999
  onChangePlacement: (next) => void changePlacement(next),
2421
4000
  onOpenAutoLink: () => setModal({ type: "autolink" }),
2422
4001
  onAddSkill: actions.requestAddSkill,
2423
- onAddSubscription: actions.requestAddSubscription
4002
+ onAddSubscription: actions.requestAddSubscription,
4003
+ onToast: (next) => setToast(next)
2424
4004
  }),
2425
4005
  error ? h("div", { className: "dshsc-banner err" }, `${error} · `, h("button", { className: "dshsc-btn ghost", onClick: () => void reload() }, t("action.retry"))) : null,
2426
4006
  data?.readOnly ? h("div", { className: "dshsc-banner warn" }, t("banner.readonly")) : null,
@@ -2460,7 +4040,14 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2460
4040
  writable,
2461
4041
  ...subscriptionActions
2462
4042
  }),
2463
- busy ? h("div", { className: "dshsc-meta" }, `${t("busy.working")} ${busy}`) : null,
4043
+ busy ? h("div", { className: "dshsc-meta" }, `${t("busy.working")} ${busy}`) : null
4044
+ );
4045
+ const root = h(
4046
+ "div",
4047
+ { className: "dshsc-root", ref: rootRef },
4048
+ h("style", null, CSS),
4049
+ viewer?.mode === "page" ? null : listNode,
4050
+ readerNode,
2464
4051
  toast ? h("div", { className: `dshsc-toast ${toast.kind}` }, toast.text) : null,
2465
4052
  modal ? h(Modals, {
2466
4053
  t,