@morehao/dsh-skills-console 0.3.1 → 0.3.2

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