@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/README.md +21 -5
- package/client.js +1572 -19
- package/lib/core/inventory.js +9 -2
- package/lib/core/service.js +87 -15
- package/lib/core/skill-files.js +336 -0
- package/lib/routes.js +6 -1
- package/package.json +1 -1
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
|
|
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)
|
|
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("
|
|
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] =
|
|
2323
|
-
const [view, setView] =
|
|
2324
|
-
const [modal, setModal] =
|
|
2325
|
-
const [expanded, setExpanded] =
|
|
2326
|
-
const [placementBusy, setPlacementBusy] =
|
|
2327
|
-
const [placementOverride, setPlacementOverride] =
|
|
2328
|
-
const [query, setQuery] =
|
|
2329
|
-
const [
|
|
2330
|
-
const
|
|
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
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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,
|