@morehao/dsh-skills-console 0.3.7 → 0.4.0

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
@@ -86,18 +86,19 @@ var DICT = {
86
86
  "action.unsubscribe": "取消订阅",
87
87
  "action.view": "查看",
88
88
  "action.hide": "收起",
89
- "action.links": "接入设置",
89
+ "action.exports": "导出设置",
90
90
  "action.install": "安装",
91
91
  "action.expand": "展开",
92
92
  "action.collapse": "收起",
93
93
  "action.adopt": "接管",
94
94
  "action.save": "保存",
95
95
  "action.cancel": "取消",
96
+ "action.close": "关闭",
96
97
  "action.choose": "选择目录",
97
98
  "action.retry": "重新加载",
98
99
  "view.flat": "平铺",
99
100
  "view.agent": "按 Agent",
100
- autoLink: "自动接入",
101
+ exportDefault: "新安装时自动导出",
101
102
  "settings.title": "插件设置",
102
103
  "placement.label": "侧边栏入口",
103
104
  "placement.effect": "切换后立即生效",
@@ -123,23 +124,20 @@ var DICT = {
123
124
  "toast.checked": "检查完成",
124
125
  "mode.on": "启用",
125
126
  "mode.off": "停用",
126
- "mode.offHint": "停用中:DSH 看不到它,但已接入的其他 Agent 照常可用",
127
- "links.dshGlobal": "dsh(全局)",
128
- "links.required": "必选",
129
- "links.scope": "在 DSH 里的作用范围",
130
- "links.scopeGlobal": "全局(默认)",
131
- "links.scopeProjects": "仅以下项目",
132
- "links.targets": "接入到哪些 Agent",
133
- "links.detectedHint": "只显示本机已安装(目录存在)的 Agent;已删除的会从列表消失,旧接入记录保留但不展示。",
134
- "links.canonical": "技能本体:{path};下面列的是各 Agent 的接入目录(软链目标)。",
135
- "links.workspaces": "DSH 工作区",
136
- "links.noWorkspaces": "还没有 DSH 工作区,可以点下面「选择其他目录」",
137
- "links.pickDirectory": "选择其他目录…",
138
- "links.selected": "其他已选目录",
139
- "links.hint": "保存后按勾选建立或移除接入;技能本体保留在原位置,不会移动。",
140
- "autoLink.detected": "安装后自动接入",
141
- "autoLink.none": "本机没有检测到其他 Agent",
142
- "autoLink.hint": "列表里的「dsh(全局)」固定勾选、不能取消;这里只影响之后安装时的自动接入,已安装的技能请在「接入设置」里改。",
127
+ "mode.offHint": "已停用:DSH 的模型调用与 `/` 手动调用都看不到它;技能文件、导出关系与其他 Agent 都不受影响",
128
+ "visibility.hiddenHint": "已启用,但扫描目录里没有它:确认 ~/.agents/skills 下真有这个技能,或检查 DSH_AGENTS_HOME。",
129
+ "visibility.manualOnlyHint": "该技能自己声明了 `disable-model-invocation`:模型调用看不到它,手动 `/` 调用仍可用。",
130
+ "visibility.scopeMismatch": "当前有另一份同名技能排在前面(项目或用户目录);停用只影响 canonical,不会隐藏它。",
131
+ "visibility.foreignWinner": "模型看到的是另一份同名技能:{path}(它的扫描根 rank 更小,会遮住这一份)",
132
+ "exports.targets": "导出到哪些 Agent",
133
+ "exports.detectedHint": "只列出本机已安装(目录存在)的 Agent。导出只影响它们;DSH 自己看不看得见由 DSH 决定,和导出无关。",
134
+ "exports.canonical": "技能本体:{path};下面的链只是把它暴露给不直接读该目录的 Agent。",
135
+ "exports.none": "本机没有可导出的 Agent 目录",
136
+ "exports.hint": "保存后按勾选建立或移除软链;技能本体留在原位置,不会移动。",
137
+ "exports.sharedUnknown": "共享根未验证",
138
+ "exportDefault.detected": "装完后默认导出到",
139
+ "exportDefault.none": "本机没有需要导出的 Agent",
140
+ "exportDefault.hint": "默认一个都不勾:技能装进 ~/.agents/skills 后 DSH 本来就能看见。导出只用于仍只认私有目录的 Agent,且只影响之后的安装。",
143
141
  "empty.skills": "还没有安装任何技能",
144
142
  "empty.skillsHint": "在终端跑 `npx skills add owner/repo`,或点右上角「添加技能」",
145
143
  "empty.subs": "还没有添加订阅",
@@ -167,17 +165,19 @@ var DICT = {
167
165
  "form.subHint": "只记录你关注这个仓库,不会安装任何技能。",
168
166
  "subscription.defaultRef": "默认分支",
169
167
  "confirm.deleteTitle": "删除技能",
170
- "confirm.deleteBody": "会从本机移除,断开本插件建立的软链并调 skills CLI 卸载;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
168
+ "confirm.deleteBody": "会从本机移除,断开本插件建立的软链并删除 canonical / lock 条目;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
171
169
  "confirm.updateTitle": "本地有改动",
172
170
  "confirm.updateBody": "更新会覆盖这些本地改动;旧内容已备份,可回滚。",
173
171
  "error.conflictTitle": "删除被拦截",
174
172
  "busy.working": "处理中…",
175
173
  "meta.from": "来自",
176
174
  "meta.noUpstream": "无远端来源 · 不检测更新",
177
- "meta.unlinked": "未接入任何 Agent",
178
- "meta.scope": "范围",
179
- "meta.onlyProject": "仅",
180
- "meta.agentsAll": "已接入的 Agent",
175
+ "meta.noExport": "未导出",
176
+ "meta.noExportHint": "没有给不支持共享根的 Agent 建导出链;直读 ~/.agents/skills 的 Agent 装好即见,无需导出。",
177
+ "meta.modelVisible": "模型可见",
178
+ "meta.modelHidden": "模型不可见",
179
+ "meta.manualOnly": "仅手动",
180
+ "meta.agentsAll": "已导出的 Agent",
181
181
  "meta.none": "无",
182
182
  "filter.all": "全部",
183
183
  "filter.update": "可更新",
@@ -189,16 +189,15 @@ var DICT = {
189
189
  "empty.filtered": "没有匹配的技能",
190
190
  "empty.filteredHint": "换个关键词,或清除筛选条件",
191
191
  "hint.unknown": "还有 {n} 个技能未检查更新。",
192
- "group.dshGlobal": "DSH · 全局",
193
- "group.dshProject": "DSH · 仅特定项目",
192
+ "group.visible": "模型可见",
194
193
  "group.off": "已停用",
195
- "group.unlinked": "未接入 Agent",
194
+ "group.hidden": "启用但不可见",
196
195
  installed: "已安装",
197
196
  notInstalled: "未安装",
198
197
  skillCount: "个技能",
199
198
  visibleOnly: "模型可见,但不在本地技能库",
200
- visibleOnlyOther: "DSH 能看到,但不在 ~/.agents/skills",
201
- visibleOnlyBuiltin: "本插件仅管理 ~/.agents/skills 下的技能,DSH 内置技能不在管理范围内",
199
+ visibleOnlyOther: "模型能看到,但不在 ~/.agents/skills",
200
+ visibleOnlyBuiltin: "本插件仅管理 ~/.agents/skills 下的技能,内置技能不在管理范围内",
202
201
  "builtin.view": "查看内置技能({n})",
203
202
  "builtin.hide": "收起内置技能",
204
203
  external: "非本插件创建",
@@ -250,10 +249,8 @@ var DICT = {
250
249
  "viewer.info.localChangeNone": "无(与安装时一致)",
251
250
  "viewer.info.localChangeYes": "有本地改动",
252
251
  "viewer.info.links": "接入",
253
- "viewer.info.agents": "Agent",
254
- "viewer.info.scope": "范围",
255
- "viewer.info.scopeGlobal": "全局",
256
- "viewer.info.scopeProject": "仅项目",
252
+ "viewer.info.exported": "已导出",
253
+ "viewer.info.visibility": "可见性",
257
254
  "viewer.info.mode": "状态",
258
255
  "viewer.info.status": "更新",
259
256
  "viewer.info.none": "无"
@@ -273,18 +270,19 @@ var DICT = {
273
270
  "action.unsubscribe": "Unsubscribe",
274
271
  "action.view": "View",
275
272
  "action.hide": "Hide",
276
- "action.links": "Link settings",
273
+ "action.exports": "Export settings",
277
274
  "action.install": "Install",
278
275
  "action.expand": "Expand",
279
276
  "action.collapse": "Collapse",
280
277
  "action.adopt": "Adopt",
281
278
  "action.save": "Save",
282
279
  "action.cancel": "Cancel",
280
+ "action.close": "Close",
283
281
  "action.choose": "Choose folder",
284
282
  "action.retry": "Reload",
285
283
  "view.flat": "Flat",
286
284
  "view.agent": "By agent",
287
- "autoLink": "Auto-link",
285
+ "exportDefault": "Auto-export on install",
288
286
  "settings.title": "Plugin settings",
289
287
  "placement.label": "Sidebar entry",
290
288
  "placement.effect": "Applies immediately",
@@ -309,23 +307,20 @@ var DICT = {
309
307
  "toast.checked": "Checked",
310
308
  "mode.on": "Enabled",
311
309
  "mode.off": "Disabled",
312
- "mode.offHint": "Disabled: DSH cannot see it, other linked agents still can",
313
- "links.dshGlobal": "dsh (global)",
314
- "links.required": "required",
315
- "links.scope": "Scope inside DSH",
316
- "links.scopeGlobal": "Global (default)",
317
- "links.scopeProjects": "Only these projects",
318
- "links.targets": "Link to agents",
319
- "links.detectedHint": "Only installed agents are listed; deleted agents disappear but their old link records are kept and hidden.",
320
- "links.canonical": "Skill source: {path}; the entries below are each agent's link target.",
321
- "links.workspaces": "DSH workspaces",
322
- "links.noWorkspaces": 'No DSH workspaces yet; use "Choose another folder" below',
323
- "links.pickDirectory": "Choose another folder…",
324
- "links.selected": "Other selected folders",
325
- "links.hint": "Saving only adds or removes links; the skill itself stays where it is.",
326
- "autoLink.detected": "Auto-link after install",
327
- "autoLink.none": "No other agents detected on this machine",
328
- "autoLink.hint": 'The "dsh (global)" entry is always checked and cannot be unchecked; this only affects future installs. Change existing skills under "Link settings".',
310
+ "mode.offHint": "Disabled for DSH’s model and `/` calls; the skill files, exports, and other agents are untouched",
311
+ "visibility.hiddenHint": "Enabled, but no copy sits in a scan directory: check that the skill really exists under ~/.agents/skills, or inspect DSH_AGENTS_HOME.",
312
+ "visibility.manualOnlyHint": "This skill declares `disable-model-invocation`: the model cannot invoke it, but manual `/` invocation still works.",
313
+ "visibility.scopeMismatch": "Another copy of this skill ranks first (project or user directory); disabling only affects the canonical copy.",
314
+ "visibility.foreignWinner": "The model sees a different copy of this skill: {path} (its scan root ranks higher, so it shadows this one)",
315
+ "exports.targets": "Export to agents",
316
+ "exports.detectedHint": "Only installed agents (existing directories) are listed. Exports only affect them; whether DSH sees the skill is up to DSH, not exports.",
317
+ "exports.canonical": "Skill source: {path}; the entries below only expose it to agents that do not read that directory directly.",
318
+ "exports.none": "No agent directories available for export on this machine",
319
+ "exports.hint": "Saving only adds or removes links; the skill itself stays where it is.",
320
+ "exports.sharedUnknown": "shared root unverified",
321
+ "exportDefault.detected": "Export new installs to",
322
+ "exportDefault.none": "No agents need exporting on this machine",
323
+ "exportDefault.hint": "Nothing is checked by default: once a skill lands in ~/.agents/skills, DSH already sees it. Exports only matter for agents that still read their own directory, and only affect future installs.",
329
324
  "empty.skills": "No skills installed yet",
330
325
  "empty.skillsHint": 'Run `npx skills add owner/repo` or use "Add skill".',
331
326
  "empty.subs": "No subscriptions yet",
@@ -353,17 +348,19 @@ var DICT = {
353
348
  "form.subHint": "Only records the repository; installs nothing.",
354
349
  "subscription.defaultRef": "default branch",
355
350
  "confirm.deleteTitle": "Delete skill",
356
- "confirm.deleteBody": "Removes it locally, disconnects this plugin’s links and calls skills remove. A snapshot stays in trash for 7 days. Your source repo is untouched.",
351
+ "confirm.deleteBody": "Removes it locally, disconnects this plugin’s links, and deletes the canonical directory and lock entry. A snapshot stays in trash for 7 days. Your source repo is untouched.",
357
352
  "confirm.updateTitle": "Local changes",
358
353
  "confirm.updateBody": "Updating overwrites local changes; the old content is backed up and can be rolled back.",
359
354
  "error.conflictTitle": "Delete blocked",
360
355
  "busy.working": "Working…",
361
356
  "meta.from": "from",
362
357
  "meta.noUpstream": "no upstream · updates not checked",
363
- "meta.unlinked": "not linked to any agent",
364
- "meta.scope": "scope",
365
- "meta.onlyProject": "only",
366
- "meta.agentsAll": "Connected agents",
358
+ "meta.noExport": "Not exported",
359
+ "meta.noExportHint": "No export links to agents that need them; agents that read ~/.agents/skills can use it as-is.",
360
+ "meta.modelVisible": "visible to the model",
361
+ "meta.modelHidden": "hidden from the model",
362
+ "meta.manualOnly": "Manual only",
363
+ "meta.agentsAll": "Exported agents",
367
364
  "meta.none": "none",
368
365
  "filter.all": "All",
369
366
  "filter.update": "Updates",
@@ -375,16 +372,15 @@ var DICT = {
375
372
  "empty.filtered": "No matching skills",
376
373
  "empty.filteredHint": "Try another keyword, or clear the filters",
377
374
  "hint.unknown": "{n} skills have not been checked for updates.",
378
- "group.dshGlobal": "DSH · global",
379
- "group.dshProject": "DSH · project only",
375
+ "group.visible": "Model visible",
380
376
  "group.off": "Disabled",
381
- "group.unlinked": "Not linked",
377
+ "group.hidden": "Enabled but hidden",
382
378
  installed: "installed",
383
379
  notInstalled: "not installed",
384
380
  skillCount: "skills",
385
381
  visibleOnly: "Visible to the model but not in the local skill library",
386
- visibleOnlyOther: "DSH can see these, but they are not under ~/.agents/skills",
387
- visibleOnlyBuiltin: "This plugin only manages skills under ~/.agents/skills; DSH built-in skills are out of scope",
382
+ visibleOnlyOther: "The model can see these, but they are not under ~/.agents/skills",
383
+ visibleOnlyBuiltin: "This plugin only manages skills under ~/.agents/skills; built-in skills are out of scope",
388
384
  "builtin.view": "View built-ins ({n})",
389
385
  "builtin.hide": "Hide built-ins",
390
386
  external: "created outside this plugin",
@@ -437,10 +433,8 @@ var DICT = {
437
433
  "viewer.info.localChangeNone": "None (matches install)",
438
434
  "viewer.info.localChangeYes": "Modified locally",
439
435
  "viewer.info.links": "Linked",
440
- "viewer.info.agents": "Agents",
441
- "viewer.info.scope": "Scope",
442
- "viewer.info.scopeGlobal": "Global",
443
- "viewer.info.scopeProject": "Project only",
436
+ "viewer.info.exported": "Exported",
437
+ "viewer.info.visibility": "Visibility",
444
438
  "viewer.info.mode": "State",
445
439
  "viewer.info.status": "Updates",
446
440
  "viewer.info.none": "None"
@@ -848,7 +842,26 @@ function FolderIcon({ size = 12 }) {
848
842
  }
849
843
 
850
844
  // client/section.js
851
- var import_react5 = __toESM(require("react"), 1);
845
+ var import_react6 = __toESM(require("react"), 1);
846
+
847
+ // client/scroll.js
848
+ function isScroller(node, tolerance = 4) {
849
+ if (!node || typeof getComputedStyle !== "function") return false;
850
+ const style = getComputedStyle(node);
851
+ return Boolean(style && /(auto|scroll)/.test(style.overflowY || "") && node.scrollHeight > node.clientHeight + tolerance);
852
+ }
853
+ function nearestScroller(node, tolerance = 4) {
854
+ let current = node?.parentElement || null;
855
+ while (current) {
856
+ if (isScroller(current, tolerance)) return current;
857
+ current = current.parentElement || null;
858
+ }
859
+ return typeof document !== "undefined" ? document.scrollingElement || null : null;
860
+ }
861
+ function scrollHost(node, tolerance = 4) {
862
+ if (isScroller(node, tolerance)) return node;
863
+ return node && nearestScroller(node, tolerance) || node;
864
+ }
852
865
 
853
866
  // client/styles.js
854
867
  var CSS = `
@@ -1004,6 +1017,10 @@ var CSS = `
1004
1017
  .dshsc-row.off { opacity: .7; }
1005
1018
  .dshsc-cell { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
1006
1019
  .dshsc-namerow { display: flex; align-items: center; gap: 7px; min-width: 0; }
1020
+ /* 可见性状态点:中性表达(实心=模型可见,空心=模型不可见),不绑定任何 Agent 身份 */
1021
+ .dshsc-vdot { flex: none; width: 6px; height: 6px; border-radius: 50%; box-sizing: border-box;
1022
+ background: var(--dsw-alias-state-success-primary); opacity: .75; }
1023
+ .dshsc-vdot.hidden { background: transparent; border: 1px solid var(--dsw-alias-label-tertiary); opacity: .9; }
1007
1024
  .dshsc-name { font-size: 13px; font-weight: 600; letter-spacing: -.005em; min-width: 0;
1008
1025
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1009
1026
  /* 技能名本身是查看入口:清掉 button 默认外观,保留 hover 下划线提示可点 */
@@ -1029,9 +1046,6 @@ var CSS = `
1029
1046
  .dshsc-agent { display: inline-flex; align-items: center; gap: 5px; flex: none; height: 20px; padding: 0 7px;
1030
1047
  border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-secondary);
1031
1048
  background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent); font-size: 11px; white-space: nowrap; }
1032
- .dshsc-agent.dsh { color: var(--dsw-alias-brand-primary); border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 30%, var(--dsw-alias-border-l1));
1033
- background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
1034
- .dshsc-agent .dshsc-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: .8; }
1035
1049
  .dshsc-agent.more, .dshsc-agent.scope { border-style: dashed; background: transparent; color: var(--dsw-alias-label-tertiary); }
1036
1050
  .dshsc-agent.more { margin: 0; font: inherit; font-size: 11px; line-height: 1; cursor: pointer;
1037
1051
  -webkit-appearance: none; appearance: none; }
@@ -1267,8 +1281,9 @@ var CSS = `
1267
1281
  color: var(--dsw-alias-label-secondary); line-height: 1.6; }
1268
1282
  .dshsc-reader-placeholder b { color: var(--dsw-alias-label-primary); font-size: 13px; }
1269
1283
 
1270
- /* 文件 tab:左树右预览;抽屉里上下排 */
1271
- .dshsc-reader-files { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
1284
+ /* 文件 tab:抽屉里上下排;整页(宽容器)左树右预览 */
1285
+ .dshsc-reader-files { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
1286
+ .dshsc-reader-page .dshsc-reader-files { grid-template-columns: minmax(200px, 240px) minmax(0, 1fr); }
1272
1287
  .dshsc-reader-tree { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: auto;
1273
1288
  max-height: 300px; background: var(--dsw-alias-bg-layer-1); }
1274
1289
  .dshsc-reader-preview { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: hidden;
@@ -1386,8 +1401,11 @@ var CSS = `
1386
1401
  background: var(--dsw-alias-bg-layer-1); }
1387
1402
  .dshsc-reader-hero .dshsc-reader-head { border-bottom: 0; }
1388
1403
  .dshsc-reader-hero .dshsc-reader-desc { -webkit-line-clamp: 3; max-width: 78ch; }
1389
- .dshsc-reader-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 180px minmax(0, 1fr);
1404
+ .dshsc-reader-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr);
1390
1405
  gap: 18px; align-items: stretch; }
1406
+ /* 只有目录真的在的时候才开第二轨:否则唯一的正文列会被自动摆进 180px 的目录轨,
1407
+ 文件 / 信息 tab 会窄成一条(树里的文件名被截断、预览挤到下面)。 */
1408
+ .dshsc-reader-layout.has-toc { grid-template-columns: 180px minmax(0, 1fr); }
1391
1409
  .dshsc-reader-toc { position: sticky; top: 0; display: flex; flex-direction: column; gap: 1px;
1392
1410
  max-height: 70vh; overflow: auto; padding-right: 4px; }
1393
1411
  .dshsc-reader-toc-title { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase;
@@ -1413,11 +1431,20 @@ var CSS = `
1413
1431
  @container dshsc (max-width: 860px) {
1414
1432
  /* 行内「查看」先让位:技能名本身仍是入口 */
1415
1433
  .dshsc-row .dshsc-view { display: none; }
1416
- .dshsc-reader-layout { grid-template-columns: minmax(0, 1fr); }
1434
+ .dshsc-reader-layout.has-toc { grid-template-columns: minmax(0, 1fr); }
1435
+ .dshsc-reader-page .dshsc-reader-files { grid-template-columns: minmax(0, 1fr); }
1417
1436
  .dshsc-reader-toc { display: none; }
1418
1437
  .dshsc-reader-page .dshsc-reader-desc { -webkit-line-clamp: 2; }
1419
1438
  }
1420
1439
 
1440
+ /* 描述默认按行数截断;真被截断时下面给「展开 / 收起」,所以要压在上面所有 clamp 规则之后 */
1441
+ .dshsc-reader-descblock { display: flex; flex-direction: column; align-items: flex-start; }
1442
+ .dshsc-reader-desc.expanded { display: block; overflow: visible; -webkit-line-clamp: unset; }
1443
+ .dshsc-reader-more { margin-top: 3px; padding: 0; border: 0; border-radius: 4px; background: transparent;
1444
+ color: var(--dsw-alias-brand-primary); font: inherit; font-size: 11.5px; line-height: 1.6; cursor: pointer; }
1445
+ .dshsc-reader-more:hover { text-decoration: underline; }
1446
+ .dshsc-reader-more:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1447
+
1421
1448
  .dshsc-toast { position: fixed; right: 18px; bottom: 18px; z-index: 70; font-size: 12px; padding: 10px 13px; border-radius: 9px;
1422
1449
  background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2);
1423
1450
  box-shadow: 0 8px 24px rgba(0,0,0,.22); max-width: 420px; }
@@ -1435,7 +1462,7 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1435
1462
  });
1436
1463
  }
1437
1464
  async function toggleMode(row) {
1438
- const mode = row.mode === "on" ? "off" : "on";
1465
+ const mode = row.enabled === false ? "on" : "off";
1439
1466
  await run(`mode:${row.name}`, () => apiCall("POST", `/skills/${encodeURIComponent(row.name)}/mode`, { mode }));
1440
1467
  }
1441
1468
  async function updateOne(row, overwrite) {
@@ -1498,17 +1525,20 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1498
1525
  }
1499
1526
  });
1500
1527
  }
1501
- async function saveLinks(row, targets, scopeProjects) {
1502
- const current = new Set(row.links.map((link) => link.target));
1503
- const desired = new Set(scopeProjects.length > 0 ? targets.filter((target) => target !== "dsh-global") : targets);
1528
+ async function saveExports(row, targets) {
1529
+ const current = new Set(row.exports.map((item) => item.target));
1530
+ const desired = new Set(targets);
1504
1531
  const toRemove = [...current].filter((target) => !desired.has(target));
1505
- await run(`links:${row.name}`, async () => {
1532
+ const toAdd = [...desired].filter((target) => !current.has(target));
1533
+ await run(`exports:${row.name}`, async () => {
1506
1534
  if (toRemove.length > 0) {
1507
- await apiCall("POST", "/links", { skill: row.name, targets: toRemove, action: "remove" });
1535
+ await apiCall("POST", "/exports", { skill: row.name, targets: toRemove, action: "remove" });
1536
+ }
1537
+ if (toAdd.length > 0) {
1538
+ await apiCall("POST", "/exports", { skill: row.name, targets: toAdd, action: "add" });
1508
1539
  }
1509
- await apiCall("POST", "/links", { skill: row.name, targets: [...desired], action: "add", scopeProjects });
1510
1540
  });
1511
- setToast({ kind: "ok", text: `${row.name} · ${t("action.links")} ✓` });
1541
+ setToast({ kind: "ok", text: `${row.name} · ${t("action.exports")} ✓` });
1512
1542
  }
1513
1543
  function requestAddSkill() {
1514
1544
  setModal({ type: "add-skill" });
@@ -1566,7 +1596,7 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1566
1596
  updateAll,
1567
1597
  checkUpdates,
1568
1598
  requestDelete,
1569
- saveLinks,
1599
+ saveExports,
1570
1600
  requestAddSkill,
1571
1601
  requestAddSubscription,
1572
1602
  probeSkills,
@@ -1913,10 +1943,10 @@ function Header({
1913
1943
  h("button", {
1914
1944
  className: "dshsc-btn",
1915
1945
  disabled: !writable,
1916
- title: `${t("autoLink")}:${autoLinkLabel}`,
1917
- "aria-label": `${t("autoLink")}:${autoLinkLabel}`,
1946
+ title: `${t("exportDefault")}:${autoLinkLabel}`,
1947
+ "aria-label": `${t("exportDefault")}:${autoLinkLabel}`,
1918
1948
  onClick: onOpenAutoLink
1919
- }, `${t("autoLink")} · ${autoLinkCount} ▾`),
1949
+ }, `${t("exportDefault")} · ${autoLinkCount} ▾`),
1920
1950
  h("button", {
1921
1951
  className: "dshsc-btn primary",
1922
1952
  disabled: !writable,
@@ -2006,6 +2036,7 @@ function InstalledTab({
2006
2036
  actions
2007
2037
  }) {
2008
2038
  const updatable = (row) => UPDATABLE.has(row.status);
2039
+ const sharedTargets = new Set((data?.agents?.detected || []).filter((agent) => agent.kind === "agent" && agent.sharedHome === "yes").map((agent) => agent.target));
2009
2040
  function statusTag(row) {
2010
2041
  if (row.status === "unknown") return null;
2011
2042
  if (row.status === "no-upstream") {
@@ -2037,21 +2068,25 @@ function InstalledTab({
2037
2068
  );
2038
2069
  }
2039
2070
  function agentChips(row) {
2040
- const links = row.links.filter((link) => !link.broken);
2041
- const projectLinks = links.filter((link) => String(link.target).startsWith("project:"));
2042
- const labels = links.filter((link) => !String(link.target).startsWith("project:")).map((link) => link.target === "dsh-global" ? "DSH" : link.label);
2043
- if (projectLinks.length > 0 && !labels.includes("DSH")) labels.unshift("DSH");
2044
- const projects = row.scopeProjects || [];
2045
- const scopeNames = projects.length > 0 ? projects.map((project) => project.split("/").pop()) : projectLinks.map((link) => link.label);
2046
- if (labels.length === 0 && scopeNames.length === 0) {
2047
- return h("span", { className: "dshsc-unlinked" }, t("meta.unlinked"));
2071
+ const exported = (row.exports || []).filter(
2072
+ (item) => !item.broken && !sharedTargets.has(item.target)
2073
+ );
2074
+ if (exported.length === 0) {
2075
+ return [h("span", {
2076
+ key: "none",
2077
+ className: "dshsc-unlinked",
2078
+ title: t("meta.noExportHint")
2079
+ }, t("meta.noExport"))];
2048
2080
  }
2049
- const chips = labels.slice(0, 3).map((label, index) => h("span", {
2050
- className: `dshsc-agent${label === "DSH" ? " dsh" : ""}`,
2081
+ const chips = [];
2082
+ const labels = exported.map((item) => item.label);
2083
+ const shown = exported.slice(0, 3);
2084
+ shown.forEach((item, index) => chips.push(h("span", {
2085
+ className: "dshsc-agent",
2051
2086
  key: `agent-${index}`,
2052
- title: label
2053
- }, label === "DSH" ? h("i", { className: "dshsc-dot" }) : null, label));
2054
- if (labels.length > 3) {
2087
+ title: item.label
2088
+ }, item.label)));
2089
+ if (exported.length > shown.length) {
2055
2090
  const open = agentTip?.rowName === row.name;
2056
2091
  chips.push(h("button", {
2057
2092
  type: "button",
@@ -2065,21 +2100,17 @@ function InstalledTab({
2065
2100
  onFocus: (event) => actions.onOpenAgentTip(event, row.name, labels),
2066
2101
  onMouseLeave: actions.onCloseAgentTip,
2067
2102
  onBlur: actions.onCloseAgentTip
2068
- }, `+${labels.length - 3}`));
2069
- }
2070
- if (scopeNames.length > 0) {
2071
- chips.push(h("span", {
2072
- className: "dshsc-agent scope",
2073
- key: "scope",
2074
- title: `${t("meta.scope")}:${projects.length > 0 ? projects.join("、") : scopeNames.join("、")}`
2075
- }, `${t("meta.onlyProject")} ${scopeNames.join("、")}`));
2103
+ }, `+${exported.length - shown.length}`));
2076
2104
  }
2077
2105
  return chips;
2078
2106
  }
2079
2107
  function rowView(row) {
2080
- const off = row.mode === "off";
2108
+ const off = row.enabled === false;
2081
2109
  const tracked = row.status !== "unmanaged";
2082
2110
  const canUpdate = writable && updatable(row);
2111
+ const hidden = !off && !row.visible;
2112
+ const manualOnly = hidden && Boolean(row.visibility?.selfHidden);
2113
+ const visLabel = hidden ? t("meta.modelHidden") : t("meta.modelVisible");
2083
2114
  return h(
2084
2115
  "div",
2085
2116
  { className: `dshsc-row${off ? " off" : ""}`, key: row.name },
@@ -2089,6 +2120,12 @@ function InstalledTab({
2089
2120
  h(
2090
2121
  "div",
2091
2122
  { className: "dshsc-namerow" },
2123
+ off ? null : h("span", {
2124
+ className: `dshsc-vdot${hidden ? " hidden" : ""}`,
2125
+ role: "img",
2126
+ title: visLabel,
2127
+ "aria-label": visLabel
2128
+ }),
2092
2129
  h("button", {
2093
2130
  type: "button",
2094
2131
  className: "dshsc-name dshsc-namebtn",
@@ -2097,10 +2134,25 @@ function InstalledTab({
2097
2134
  onClick: () => actions.onView(row)
2098
2135
  }, row.name),
2099
2136
  statusTag(row),
2137
+ manualOnly ? h("span", {
2138
+ className: "dshsc-tag off",
2139
+ title: t("visibility.manualOnlyHint"),
2140
+ "aria-label": t("visibility.manualOnlyHint")
2141
+ }, t("meta.manualOnly")) : null,
2100
2142
  off ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null
2101
2143
  ),
2102
2144
  sourceLine(row),
2103
2145
  off ? h("div", { className: "dshsc-note" }, t("mode.offHint")) : null,
2146
+ // manual-only 用名字旁的「仅手动」标志表达,不占一行警告;
2147
+ // 被更靠前的同名副本盖住时由下面的 foreignWinner 点名,这里不再补「扫描目录里没有它」。
2148
+ hidden && !manualOnly && row.visibility?.winnerIsCanonical !== false ? h("div", { className: "dshsc-note warn" }, t("visibility.hiddenHint")) : null,
2149
+ row.visibility?.shadowScopeMismatch ? h("div", { className: "dshsc-note warn" }, t("visibility.scopeMismatch")) : null,
2150
+ // 同名副本在更小的 rank 上:模型看到的不是这一份,必须点名路径
2151
+ row.visibility?.winnerIsCanonical === false ? h(
2152
+ "div",
2153
+ { className: "dshsc-note warn" },
2154
+ t("visibility.foreignWinner").replace("{path}", row.visibility.winnerPath || "?")
2155
+ ) : null,
2104
2156
  row.status === "broken-link" ? h("div", { className: "dshsc-note warn" }, t("status.brokenHint")) : null
2105
2157
  ),
2106
2158
  h("div", { className: "dshsc-agents" }, agentChips(row)),
@@ -2110,12 +2162,12 @@ function InstalledTab({
2110
2162
  h("button", {
2111
2163
  className: "dshsc-switch",
2112
2164
  role: "switch",
2113
- "aria-checked": String(tracked && row.mode === "on"),
2165
+ "aria-checked": String(tracked && row.enabled !== false),
2114
2166
  "aria-label": `${row.name} ${t("mode.on")}/${t("mode.off")}`,
2115
2167
  // 未纳入追踪时调 setMode 只会报「没有安装记录」,不如直接禁用
2116
2168
  disabled: busy !== null || !tracked,
2117
2169
  onClick: () => void actions.onToggleMode(row),
2118
- title: tracked ? row.mode === "on" ? t("mode.off") : t("mode.on") : t("adopt.switchHint")
2170
+ title: tracked ? row.enabled !== false ? t("mode.off") : t("mode.on") : t("adopt.switchHint")
2119
2171
  }, h("span", { className: "dshsc-knob" })),
2120
2172
  h("button", {
2121
2173
  className: "dshsc-btn ghost dshsc-view",
@@ -2134,7 +2186,7 @@ function InstalledTab({
2134
2186
  disabled: !canUpdate || busy !== null,
2135
2187
  onClick: () => actions.onUpdate(row)
2136
2188
  }, t("action.update")),
2137
- h("button", { className: "dshsc-btn ghost", disabled: busy !== null, onClick: () => actions.onLinks(row) }, t("action.links")),
2189
+ h("button", { className: "dshsc-btn ghost", disabled: busy !== null, onClick: () => actions.onExports(row) }, t("action.exports")),
2138
2190
  h("button", {
2139
2191
  className: "dshsc-btn ghost danger sq",
2140
2192
  disabled: !writable || busy !== null,
@@ -2147,18 +2199,17 @@ function InstalledTab({
2147
2199
  }
2148
2200
  function groupedRows() {
2149
2201
  const groups = [];
2150
- const hasLink = (row, prefix) => row.links.some((link) => !link.broken && String(link.target).startsWith(prefix));
2151
- const dshRows = visibleRows.filter((row) => row.mode === "on" && !row.scopeProjects?.length && hasLink(row, "dsh-global"));
2152
- const projectRows = visibleRows.filter((row) => row.mode === "on" && row.scopeProjects?.length);
2153
- const offRows = visibleRows.filter((row) => row.mode === "off");
2154
- const unlinkedRows = visibleRows.filter((row) => row.mode === "on" && !row.scopeProjects?.length && !row.links.some((link) => !link.broken));
2155
- groups.push({ key: "dsh-global", title: t("group.dshGlobal"), rows: dshRows, dot: true });
2156
- if (projectRows.length) groups.push({ key: "dsh-project", title: t("group.dshProject"), rows: projectRows, dot: true });
2202
+ const visibleGroup = visibleRows.filter((row) => row.enabled !== false && row.visible);
2203
+ const offRows = visibleRows.filter((row) => row.enabled === false);
2204
+ const hiddenRows = visibleRows.filter((row) => row.enabled !== false && !row.visible);
2205
+ groups.push({ key: "visible", title: t("group.visible"), rows: visibleGroup, dot: true });
2157
2206
  if (offRows.length) groups.push({ key: "off", title: t("group.off"), rows: offRows });
2158
- if (unlinkedRows.length) groups.push({ key: "unlinked", title: t("group.unlinked"), rows: unlinkedRows });
2207
+ if (hiddenRows.length) groups.push({ key: "hidden", title: t("group.hidden"), rows: hiddenRows });
2159
2208
  for (const agent of data?.agents?.detected || []) {
2160
2209
  if (agent.kind !== "agent") continue;
2161
- const matched = visibleRows.filter((row) => row.links.some((link) => link.target === agent.target));
2210
+ const matched = visibleRows.filter(
2211
+ (row) => (row.exports || []).some((item) => item.target === agent.target && !item.broken)
2212
+ );
2162
2213
  if (matched.length) groups.push({ key: agent.target, title: agent.label, rows: matched });
2163
2214
  }
2164
2215
  return groups.filter((group) => group.rows.length > 0);
@@ -2313,6 +2364,7 @@ function InstalledTab({
2313
2364
 
2314
2365
  // client/views/agent-tip.js
2315
2366
  function AgentTip({ t, agentTip, agentTipPos, nodeRef }) {
2367
+ const labels = agentTip.labels || [];
2316
2368
  const node = h(
2317
2369
  "div",
2318
2370
  {
@@ -2326,15 +2378,16 @@ function AgentTip({ t, agentTip, agentTipPos, nodeRef }) {
2326
2378
  "div",
2327
2379
  { className: "dshsc-agenttip-head" },
2328
2380
  h("span", null, t("meta.agentsAll")),
2329
- h("span", { className: "dshsc-agenttip-n" }, agentTip.labels.length)
2381
+ h("span", { className: "dshsc-agenttip-n" }, labels.length)
2330
2382
  ),
2331
2383
  h(
2332
2384
  "div",
2333
2385
  { className: "dshsc-agenttip-list" },
2334
- agentTip.labels.map((label, index) => h("span", {
2335
- className: `dshsc-agent${label === "DSH" ? " dsh" : ""}`,
2336
- key: `agenttip-${index}`
2337
- }, label === "DSH" ? h("i", { className: "dshsc-dot" }) : null, label))
2386
+ labels.map((label, index) => h("span", {
2387
+ className: "dshsc-agent",
2388
+ key: `agenttip-${index}`,
2389
+ title: label
2390
+ }, label))
2338
2391
  )
2339
2392
  );
2340
2393
  return portalize(node);
@@ -2447,7 +2500,12 @@ function SubscriptionsTab({
2447
2500
  }
2448
2501
 
2449
2502
  // client/views/modals.js
2503
+ var import_react4 = __toESM(require("react"), 1);
2504
+
2505
+ // client/views/add-skill.js
2450
2506
  var import_react3 = __toESM(require("react"), 1);
2507
+
2508
+ // client/views/overlay.js
2451
2509
  function Overlay({ t, title, children, footer, onCancel }) {
2452
2510
  useOverlayLayer(true, onCancel);
2453
2511
  return portalize(h(
@@ -2472,212 +2530,8 @@ function Overlay({ t, title, children, footer, onCancel }) {
2472
2530
  )
2473
2531
  ), overlayHost());
2474
2532
  }
2475
- function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
2476
- const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
2477
- const incomplete = fields.some(
2478
- (field) => field.required !== false && !String(form[field.key] || "").trim()
2479
- );
2480
- return h(
2481
- Overlay,
2482
- {
2483
- t,
2484
- title,
2485
- onCancel,
2486
- footer: [
2487
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2488
- h("button", {
2489
- key: "s",
2490
- className: "dshsc-btn primary",
2491
- disabled: incomplete,
2492
- onClick: () => onSubmit(form)
2493
- }, t("action.save"))
2494
- ]
2495
- },
2496
- hint ? h("p", { className: "dshsc-sub" }, hint) : null,
2497
- ...fields.map((field) => h(
2498
- "label",
2499
- { className: "dshsc-field", key: field.key },
2500
- h("span", null, field.label),
2501
- h("input", {
2502
- value: form[field.key],
2503
- placeholder: field.placeholder,
2504
- onChange: (event) => setForm((prev) => ({ ...prev, [field.key]: event.target.value }))
2505
- })
2506
- ))
2507
- );
2508
- }
2509
- function ConfirmModal({ t, title, body, onCancel, onConfirm, danger, confirmLabel }) {
2510
- return h(Overlay, {
2511
- t,
2512
- title,
2513
- onCancel,
2514
- footer: [
2515
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2516
- h("button", { key: "o", className: `dshsc-btn${danger ? " danger" : " primary"}`, onClick: onConfirm }, confirmLabel || t("action.save"))
2517
- ]
2518
- }, h("p", { className: "dshsc-sub", style: { whiteSpace: "pre-wrap" } }, body));
2519
- }
2520
- function LinksModal({ t, row, agents, workspaces, pickDirectory, onCancel, onSave }) {
2521
- const candidates = agents.filter(
2522
- (agent) => agent.kind === "dsh" || agent.kind !== "project" && agent.exists
2523
- );
2524
- const [targets, setTargets] = import_react3.default.useState(() => {
2525
- const visible = new Set(candidates.map((agent) => agent.target));
2526
- const initial = new Set(row.links.map((link) => link.target).filter((target) => visible.has(target)));
2527
- initial.add("dsh-global");
2528
- return initial;
2529
- });
2530
- const [scopeProjects, setScopeProjects] = import_react3.default.useState(row.scopeProjects || []);
2531
- const [scopeMode, setScopeMode] = import_react3.default.useState((row.scopeProjects || []).length > 0 ? "projects" : "global");
2532
- const workspacePaths = new Set(workspaces.map((workspace) => workspace.path));
2533
- const otherProjects = scopeProjects.filter((project) => !workspacePaths.has(project));
2534
- const toggle = (target) => setTargets((prev) => {
2535
- const next = new Set(prev);
2536
- if (next.has(target)) next.delete(target);
2537
- else next.add(target);
2538
- return next;
2539
- });
2540
- const toggleProject = (project) => setScopeProjects((prev) => prev.includes(project) ? prev.filter((item) => item !== project) : [...prev, project]);
2541
- const chooseDirectory = async () => {
2542
- const dir = await pickDirectory?.();
2543
- if (!dir) return;
2544
- setScopeProjects((prev) => prev.includes(dir) ? prev : [...prev, dir]);
2545
- };
2546
- return h(
2547
- Overlay,
2548
- {
2549
- t,
2550
- title: `${t("action.links")} · ${row.name}`,
2551
- onCancel,
2552
- footer: [
2553
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2554
- h("button", {
2555
- key: "s",
2556
- className: "dshsc-btn primary",
2557
- onClick: () => onSave([...targets], scopeMode === "projects" ? scopeProjects.filter((item) => item.trim()) : [])
2558
- }, t("action.save"))
2559
- ]
2560
- },
2561
- h("div", { className: "dshsc-group" }, t("links.targets")),
2562
- h("p", { className: "dshsc-sub" }, t("links.detectedHint")),
2563
- h("p", { className: "dshsc-sub" }, t("links.canonical").replace("{path}", `~/.agents/skills/${row.name}`)),
2564
- ...candidates.map((agent) => h(
2565
- "label",
2566
- { className: `dshsc-check${agent.target === "dsh-global" ? " locked" : ""}`, key: agent.target },
2567
- h("input", {
2568
- type: "checkbox",
2569
- checked: agent.target === "dsh-global" ? true : targets.has(agent.target),
2570
- disabled: agent.target === "dsh-global",
2571
- onChange: () => toggle(agent.target)
2572
- }),
2573
- h("span", null, agent.kind === "dsh" ? t("links.dshGlobal") : agent.label),
2574
- agent.target === "dsh-global" ? h("small", null, t("links.required")) : null,
2575
- h("small", { title: agent.dir }, agent.dir)
2576
- )),
2577
- h("div", { className: "dshsc-group" }, t("links.scope")),
2578
- h(
2579
- "label",
2580
- { className: "dshsc-check" },
2581
- h("input", { type: "radio", name: `scope-${row.name}`, checked: scopeMode === "global", onChange: () => setScopeMode("global") }),
2582
- h("span", null, t("links.scopeGlobal"))
2583
- ),
2584
- h(
2585
- "label",
2586
- { className: "dshsc-check" },
2587
- h("input", { type: "radio", name: `scope-${row.name}`, checked: scopeMode === "projects", onChange: () => setScopeMode("projects") }),
2588
- h("span", null, t("links.scopeProjects"))
2589
- ),
2590
- scopeMode === "projects" ? h(
2591
- "div",
2592
- null,
2593
- h("div", { className: "dshsc-group" }, t("links.workspaces")),
2594
- workspaces.length === 0 ? h("p", { className: "dshsc-sub" }, t("links.noWorkspaces")) : h("div", { className: "dshsc-panel" }, workspaces.map((workspace) => h(
2595
- "label",
2596
- { className: "dshsc-check", key: workspace.id },
2597
- h("input", {
2598
- type: "checkbox",
2599
- checked: scopeProjects.includes(workspace.path),
2600
- onChange: () => toggleProject(workspace.path)
2601
- }),
2602
- h("span", null, workspace.title),
2603
- h("small", null, workspace.path)
2604
- ))),
2605
- otherProjects.length > 0 ? h("div", { className: "dshsc-group" }, t("links.selected")) : null,
2606
- ...otherProjects.map((project) => h(
2607
- "div",
2608
- { className: "dshsc-sub-entry", key: project },
2609
- h("span", { className: "dshsc-code" }, project),
2610
- h(
2611
- "div",
2612
- { className: "dshsc-actions" },
2613
- h("button", {
2614
- className: "dshsc-btn ghost danger",
2615
- onClick: () => setScopeProjects((prev) => prev.filter((item) => item !== project))
2616
- }, t("action.delete"))
2617
- )
2618
- )),
2619
- h("button", { className: "dshsc-btn", style: { alignSelf: "flex-start", marginTop: 6 }, onClick: () => void chooseDirectory() }, t("links.pickDirectory"))
2620
- ) : null,
2621
- h("p", { className: "dshsc-sub" }, t("links.hint"))
2622
- );
2623
- }
2624
- function AutoLinkModal({ t, settings, agents, dsh, onCancel, onSave }) {
2625
- const [targets, setTargets] = import_react3.default.useState(() => new Set(
2626
- Array.isArray(settings.autoLinkTargets) ? settings.autoLinkTargets : agents.map((agent) => agent.target)
2627
- ));
2628
- const toggle = (target) => setTargets((prev) => {
2629
- const next = new Set(prev);
2630
- if (next.has(target)) next.delete(target);
2631
- else next.add(target);
2632
- return next;
2633
- });
2634
- return h(
2635
- Overlay,
2636
- {
2637
- t,
2638
- title: t("autoLink"),
2639
- onCancel,
2640
- footer: [
2641
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2642
- h("button", { key: "s", className: "dshsc-btn primary", onClick: () => onSave([...targets]) }, t("action.save"))
2643
- ]
2644
- },
2645
- h("div", { className: "dshsc-group" }, t("autoLink.detected")),
2646
- h(
2647
- "div",
2648
- { className: "dshsc-panel" },
2649
- // dsh(全局)始终接入:列表里也勾上,只是不能改,勾选数与页头的计数器才对得上
2650
- h(
2651
- "label",
2652
- { className: "dshsc-check locked", key: "dsh-global" },
2653
- h("input", {
2654
- type: "checkbox",
2655
- checked: true,
2656
- disabled: true,
2657
- readOnly: true,
2658
- onChange: () => {
2659
- }
2660
- }),
2661
- h("span", null, dsh?.label || t("links.dshGlobal")),
2662
- h("small", null, t("links.required")),
2663
- dsh?.dir ? h("small", { title: dsh.dir }, dsh.dir) : null
2664
- ),
2665
- ...agents.map((agent) => h(
2666
- "label",
2667
- { className: "dshsc-check", key: agent.target },
2668
- h("input", {
2669
- type: "checkbox",
2670
- checked: targets.has(agent.target),
2671
- onChange: () => toggle(agent.target)
2672
- }),
2673
- h("span", null, agent.label),
2674
- h("small", null, agent.dir)
2675
- ))
2676
- ),
2677
- agents.length === 0 ? h("p", { className: "dshsc-sub" }, t("autoLink.none")) : null,
2678
- h("p", { className: "dshsc-sub" }, t("autoLink.hint"))
2679
- );
2680
- }
2533
+
2534
+ // client/views/add-skill.js
2681
2535
  function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
2682
2536
  const [source, setSource] = import_react3.default.useState("");
2683
2537
  const [phase, setPhase] = import_react3.default.useState("idle");
@@ -2818,7 +2672,135 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
2818
2672
  h("p", { className: "dshsc-sub" }, t("form.hint"))
2819
2673
  );
2820
2674
  }
2821
- function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, actions, onClose, onSaveAutoLink }) {
2675
+
2676
+ // client/views/modals.js
2677
+ function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
2678
+ const [form, setForm] = import_react4.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
2679
+ const incomplete = fields.some(
2680
+ (field) => field.required !== false && !String(form[field.key] || "").trim()
2681
+ );
2682
+ return h(
2683
+ Overlay,
2684
+ {
2685
+ t,
2686
+ title,
2687
+ onCancel,
2688
+ footer: [
2689
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2690
+ h("button", {
2691
+ key: "s",
2692
+ className: "dshsc-btn primary",
2693
+ disabled: incomplete,
2694
+ onClick: () => onSubmit(form)
2695
+ }, t("action.save"))
2696
+ ]
2697
+ },
2698
+ hint ? h("p", { className: "dshsc-sub" }, hint) : null,
2699
+ ...fields.map((field) => h(
2700
+ "label",
2701
+ { className: "dshsc-field", key: field.key },
2702
+ h("span", null, field.label),
2703
+ h("input", {
2704
+ value: form[field.key],
2705
+ placeholder: field.placeholder,
2706
+ onChange: (event) => setForm((prev) => ({ ...prev, [field.key]: event.target.value }))
2707
+ })
2708
+ ))
2709
+ );
2710
+ }
2711
+ function ConfirmModal({ t, title, body, onCancel, onConfirm, danger, confirmLabel, hideCancel = false }) {
2712
+ return h(Overlay, {
2713
+ t,
2714
+ title,
2715
+ onCancel,
2716
+ footer: [
2717
+ ...hideCancel ? [] : [h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel"))],
2718
+ h("button", { key: "o", className: `dshsc-btn${danger ? " danger" : " primary"}`, onClick: onConfirm }, confirmLabel || t("action.save"))
2719
+ ]
2720
+ }, h("p", { className: "dshsc-sub", style: { whiteSpace: "pre-wrap" } }, body));
2721
+ }
2722
+ function ExportsModal({ t, row, agents, onCancel, onSave }) {
2723
+ const candidates = agents.filter(
2724
+ (agent) => agent.kind === "agent" && agent.exists && agent.exportable
2725
+ );
2726
+ const [targets, setTargets] = import_react4.default.useState(() => {
2727
+ const available = new Set(candidates.map((agent) => agent.target));
2728
+ return new Set((row.exports || []).map((item) => item.target).filter((target) => available.has(target)));
2729
+ });
2730
+ const toggle = (target) => setTargets((prev) => {
2731
+ const next = new Set(prev);
2732
+ if (next.has(target)) next.delete(target);
2733
+ else next.add(target);
2734
+ return next;
2735
+ });
2736
+ return h(
2737
+ Overlay,
2738
+ {
2739
+ t,
2740
+ title: `${t("action.exports")} · ${row.name}`,
2741
+ onCancel,
2742
+ footer: [
2743
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2744
+ h("button", { key: "s", className: "dshsc-btn primary", onClick: () => onSave([...targets]) }, t("action.save"))
2745
+ ]
2746
+ },
2747
+ h("div", { className: "dshsc-group" }, t("exports.targets")),
2748
+ h("p", { className: "dshsc-sub" }, t("exports.detectedHint")),
2749
+ h("p", { className: "dshsc-sub" }, t("exports.canonical").replace("{path}", `~/.agents/skills/${row.name}`)),
2750
+ candidates.length === 0 ? h("p", { className: "dshsc-sub" }, t("exports.none")) : null,
2751
+ ...candidates.map((agent) => h(
2752
+ "label",
2753
+ { className: "dshsc-check", key: agent.target },
2754
+ h("input", {
2755
+ type: "checkbox",
2756
+ checked: targets.has(agent.target),
2757
+ onChange: () => toggle(agent.target)
2758
+ }),
2759
+ h("span", null, agent.label),
2760
+ // 候选里已经没有「读共享根」的 agent 了;'no' 是常态不必标注,只提示没验证过的
2761
+ agent.sharedHome === "unknown" ? h("small", null, t("exports.sharedUnknown")) : null,
2762
+ h("small", { title: agent.dir }, agent.dir)
2763
+ )),
2764
+ h("p", { className: "dshsc-sub" }, t("exports.hint"))
2765
+ );
2766
+ }
2767
+ function DefaultExportModal({ t, settings, agents, onCancel, onSave }) {
2768
+ const [targets, setTargets] = import_react4.default.useState(() => new Set(
2769
+ Array.isArray(settings.exportTargets) ? settings.exportTargets : []
2770
+ ));
2771
+ const toggle = (target) => setTargets((prev) => {
2772
+ const next = new Set(prev);
2773
+ if (next.has(target)) next.delete(target);
2774
+ else next.add(target);
2775
+ return next;
2776
+ });
2777
+ return h(
2778
+ Overlay,
2779
+ {
2780
+ t,
2781
+ title: t("exportDefault"),
2782
+ onCancel,
2783
+ footer: [
2784
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2785
+ h("button", { key: "s", className: "dshsc-btn primary", onClick: () => onSave([...targets]) }, t("action.save"))
2786
+ ]
2787
+ },
2788
+ h("div", { className: "dshsc-group" }, t("exportDefault.detected")),
2789
+ agents.length === 0 ? h("p", { className: "dshsc-sub" }, t("exportDefault.none")) : h("div", { className: "dshsc-panel" }, agents.map((agent) => h(
2790
+ "label",
2791
+ { className: "dshsc-check", key: agent.target },
2792
+ h("input", {
2793
+ type: "checkbox",
2794
+ checked: targets.has(agent.target),
2795
+ onChange: () => toggle(agent.target)
2796
+ }),
2797
+ h("span", null, agent.label),
2798
+ h("small", null, agent.dir)
2799
+ ))),
2800
+ h("p", { className: "dshsc-sub" }, t("exportDefault.hint"))
2801
+ );
2802
+ }
2803
+ function Modals({ t, modal, data, autoLinkAgents, actions, onClose, onSaveAutoLink }) {
2822
2804
  if (modal.type === "add-skill") {
2823
2805
  return h(AddSkillModal, {
2824
2806
  t,
@@ -2841,26 +2823,23 @@ function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, ac
2841
2823
  onSubmit: (form) => void actions.addSubscription(form)
2842
2824
  });
2843
2825
  }
2844
- if (modal.type === "links") {
2845
- return h(LinksModal, {
2826
+ if (modal.type === "exports") {
2827
+ return h(ExportsModal, {
2846
2828
  t,
2847
2829
  row: modal.row,
2848
2830
  agents: data?.agents?.all || [],
2849
- workspaces: data?.workspaces || [],
2850
- pickDirectory,
2851
2831
  onCancel: onClose,
2852
- onSave: (targets, scopeProjects) => {
2832
+ onSave: (targets) => {
2853
2833
  onClose();
2854
- void actions.saveLinks(modal.row, targets, scopeProjects);
2834
+ void actions.saveExports(modal.row, targets);
2855
2835
  }
2856
2836
  });
2857
2837
  }
2858
2838
  if (modal.type === "autolink") {
2859
- return h(AutoLinkModal, {
2839
+ return h(DefaultExportModal, {
2860
2840
  t,
2861
2841
  settings: data?.settings || {},
2862
2842
  agents: autoLinkAgents,
2863
- dsh: autoLinkDsh,
2864
2843
  onCancel: onClose,
2865
2844
  onSave: (targets) => void onSaveAutoLink(targets)
2866
2845
  });
@@ -2871,6 +2850,7 @@ function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, ac
2871
2850
  title: t("confirm.deleteTitle"),
2872
2851
  body: t("confirm.deleteBody"),
2873
2852
  danger: true,
2853
+ confirmLabel: t("action.delete"),
2874
2854
  onCancel: onClose,
2875
2855
  onConfirm: modal.onConfirm
2876
2856
  });
@@ -2880,6 +2860,7 @@ function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, ac
2880
2860
  t,
2881
2861
  title: t("confirm.updateTitle"),
2882
2862
  body: t("confirm.updateBody"),
2863
+ confirmLabel: t("action.update"),
2883
2864
  onCancel: onClose,
2884
2865
  onConfirm: modal.onConfirm
2885
2866
  });
@@ -2893,14 +2874,16 @@ function Modals({ t, modal, data, autoLinkAgents, autoLinkDsh, pickDirectory, ac
2893
2874
  ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
2894
2875
  onCancel: onClose,
2895
2876
  onConfirm: onClose,
2896
- confirmLabel: t("action.cancel")
2877
+ confirmLabel: t("action.close"),
2878
+ danger: true,
2879
+ hideCancel: true
2897
2880
  });
2898
2881
  }
2899
2882
  return null;
2900
2883
  }
2901
2884
 
2902
2885
  // client/views/skill-reader.js
2903
- var import_react4 = __toESM(require("react"), 1);
2886
+ var import_react5 = __toESM(require("react"), 1);
2904
2887
 
2905
2888
  // client/markdown.js
2906
2889
  function classifyHref(href) {
@@ -3183,7 +3166,7 @@ function resolveRelativeHref(currentDir, href) {
3183
3166
  return { relPath: out.join("/"), anchor: target.kind === "anchor" ? target.href : "" };
3184
3167
  }
3185
3168
 
3186
- // client/views/skill-reader.js
3169
+ // client/views/markdown-view.js
3187
3170
  var PARSE_CACHE = /* @__PURE__ */ new Map();
3188
3171
  function parseCached(text) {
3189
3172
  const key = String(text ?? "");
@@ -3196,49 +3179,6 @@ function parseCached(text) {
3196
3179
  }
3197
3180
  return parsed;
3198
3181
  }
3199
- function formatBytes(bytes) {
3200
- const value = Number(bytes) || 0;
3201
- if (value < 1024) return `${value} B`;
3202
- if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3203
- return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3204
- }
3205
- function formatWhen(iso) {
3206
- if (!iso) return null;
3207
- const date = new Date(iso);
3208
- if (Number.isNaN(date.getTime())) return null;
3209
- const pad = (value) => String(value).padStart(2, "0");
3210
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3211
- }
3212
- var MARKDOWN = /\.(md|markdown)$/i;
3213
- function isMarkdownPath(relPath) {
3214
- return MARKDOWN.test(String(relPath || ""));
3215
- }
3216
- function isScroller(node) {
3217
- if (!node || typeof getComputedStyle !== "function") return false;
3218
- const style = getComputedStyle(node);
3219
- return Boolean(style && /(auto|scroll)/.test(style.overflowY || "") && node.scrollHeight > node.clientHeight + 4);
3220
- }
3221
- function nearestScroller(node) {
3222
- let current = node?.parentElement || null;
3223
- while (current) {
3224
- if (isScroller(current)) return current;
3225
- current = current.parentElement || null;
3226
- }
3227
- return typeof document !== "undefined" ? document.scrollingElement || null : null;
3228
- }
3229
- function scrollHost(node) {
3230
- if (isScroller(node)) return node;
3231
- return node && nearestScroller(node) || node;
3232
- }
3233
- var STATUS_KEY = {
3234
- current: "status.current",
3235
- "update-available": "status.update",
3236
- "locally-modified": "status.local",
3237
- conflict: "status.conflict",
3238
- "upstream-deleted": "status.deleted",
3239
- "broken-link": "status.broken",
3240
- unmanaged: "status.unmanaged"
3241
- };
3242
3182
  function inlineNodes(nodes, { onOpenFile, onOpenAnchor, keyPrefix = "i" }) {
3243
3183
  if (!Array.isArray(nodes)) return null;
3244
3184
  return nodes.map((node, index) => {
@@ -3371,6 +3311,34 @@ function Markdown({ text, onOpenFile, onOpenAnchor }) {
3371
3311
  const { blocks } = parseCached(text);
3372
3312
  return h("div", { className: "dshsc-md" }, blockNodes(blocks, { onOpenFile, onOpenAnchor }));
3373
3313
  }
3314
+
3315
+ // client/views/skill-reader.js
3316
+ function formatBytes(bytes) {
3317
+ const value = Number(bytes) || 0;
3318
+ if (value < 1024) return `${value} B`;
3319
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`;
3320
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
3321
+ }
3322
+ function formatWhen(iso) {
3323
+ if (!iso) return null;
3324
+ const date = new Date(iso);
3325
+ if (Number.isNaN(date.getTime())) return null;
3326
+ const pad = (value) => String(value).padStart(2, "0");
3327
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
3328
+ }
3329
+ var MARKDOWN = /\.(md|markdown)$/i;
3330
+ function isMarkdownPath(relPath) {
3331
+ return MARKDOWN.test(String(relPath || ""));
3332
+ }
3333
+ var STATUS_KEY = {
3334
+ current: "status.current",
3335
+ "update-available": "status.update",
3336
+ "locally-modified": "status.local",
3337
+ conflict: "status.conflict",
3338
+ "upstream-deleted": "status.deleted",
3339
+ "broken-link": "status.broken",
3340
+ unmanaged: "status.unmanaged"
3341
+ };
3374
3342
  function TabButton({ id, label, count, active, onSelect }) {
3375
3343
  return h("button", {
3376
3344
  type: "button",
@@ -3407,18 +3375,21 @@ function SkillReader({
3407
3375
  onClose,
3408
3376
  onOpenPage,
3409
3377
  onUpdate,
3410
- onLinks,
3378
+ onExports,
3411
3379
  onToast
3412
3380
  }) {
3413
3381
  const { state, file, loadFile, retry } = useSkillReader(row);
3414
- const [tab, setTab] = import_react4.default.useState(initialTab);
3415
- const [format, setFormat] = import_react4.default.useState("rendered");
3416
- const [docOverride, setDocOverride] = import_react4.default.useState(null);
3417
- const [fileFormat, setFileFormat] = import_react4.default.useState("rendered");
3418
- const [tocActive, setTocActive] = import_react4.default.useState(null);
3419
- const bodyRef = import_react4.default.useRef(null);
3420
- const dialogRef = import_react4.default.useRef(null);
3421
- const scrollRef = import_react4.default.useRef(0);
3382
+ const [tab, setTab] = import_react5.default.useState(initialTab);
3383
+ const [format, setFormat] = import_react5.default.useState("rendered");
3384
+ const [docOverride, setDocOverride] = import_react5.default.useState(null);
3385
+ const [fileFormat, setFileFormat] = import_react5.default.useState("rendered");
3386
+ const [tocActive, setTocActive] = import_react5.default.useState(null);
3387
+ const [descOpen, setDescOpen] = import_react5.default.useState(false);
3388
+ const [descClamped, setDescClamped] = import_react5.default.useState(false);
3389
+ const bodyRef = import_react5.default.useRef(null);
3390
+ const dialogRef = import_react5.default.useRef(null);
3391
+ const descRef = import_react5.default.useRef(null);
3392
+ const scrollRef = import_react5.default.useRef(0);
3422
3393
  useOverlayLayer(variant === "drawer" || variant === "page", onClose);
3423
3394
  const name = row?.name || "";
3424
3395
  const tree = state.tree;
@@ -3427,20 +3398,49 @@ function SkillReader({
3427
3398
  const bodyText = docOverride ? docOverride.content : state.body?.content || "";
3428
3399
  const description = row?.description || state.body?.meta?.description || "";
3429
3400
  const headings = tab === "doc" && bodyText ? parseCached(bodyText).headings : [];
3430
- import_react4.default.useEffect(() => {
3401
+ import_react5.default.useEffect(() => {
3431
3402
  setDocOverride(null);
3432
3403
  scrollRef.current = 0;
3433
3404
  }, [name]);
3434
- import_react4.default.useEffect(() => {
3405
+ const descOwnerRef = import_react5.default.useRef(name);
3406
+ import_react5.default.useEffect(() => {
3407
+ if (descOwnerRef.current === name) return;
3408
+ descOwnerRef.current = name;
3409
+ setDescOpen(false);
3410
+ setDescClamped(false);
3411
+ }, [name]);
3412
+ import_react5.default.useLayoutEffect(() => {
3413
+ const node = descRef.current;
3414
+ if (!node) return void 0;
3415
+ const measure = () => {
3416
+ if (descOpen) return;
3417
+ const scroll = Number(node.scrollHeight) || 0;
3418
+ const client = Number(node.clientHeight) || 0;
3419
+ setDescClamped(scroll > client + 1);
3420
+ };
3421
+ measure();
3422
+ if (typeof window === "undefined" || !window.addEventListener) return void 0;
3423
+ window.addEventListener("resize", measure);
3424
+ let observer = null;
3425
+ if (typeof ResizeObserver === "function") {
3426
+ observer = new ResizeObserver(measure);
3427
+ observer.observe(node);
3428
+ }
3429
+ return () => {
3430
+ observer?.disconnect?.();
3431
+ window.removeEventListener("resize", measure);
3432
+ };
3433
+ }, [description, descOpen]);
3434
+ import_react5.default.useEffect(() => {
3435
3435
  if (!initialFile) return;
3436
3436
  setTab("files");
3437
3437
  void loadFile(initialFile);
3438
3438
  }, [initialFile, loadFile]);
3439
- import_react4.default.useEffect(() => {
3439
+ import_react5.default.useEffect(() => {
3440
3440
  const node = bodyRef.current;
3441
3441
  if (node && scrollRef.current) node.scrollTop = scrollRef.current;
3442
3442
  });
3443
- import_react4.default.useEffect(() => {
3443
+ import_react5.default.useEffect(() => {
3444
3444
  if (variant !== "drawer" || typeof document === "undefined") return void 0;
3445
3445
  const previous = document.activeElement;
3446
3446
  const node = dialogRef.current;
@@ -3467,12 +3467,12 @@ function SkillReader({
3467
3467
  previous?.focus?.();
3468
3468
  };
3469
3469
  }, [variant, onClose]);
3470
- import_react4.default.useEffect(() => {
3470
+ import_react5.default.useEffect(() => {
3471
3471
  if (variant !== "page" || typeof document === "undefined") return;
3472
3472
  const node = document.getElementById("dshsc-reader-name");
3473
3473
  node?.focus?.();
3474
3474
  }, [variant, name]);
3475
- import_react4.default.useEffect(() => {
3475
+ import_react5.default.useEffect(() => {
3476
3476
  if (variant !== "page" || typeof window === "undefined") return void 0;
3477
3477
  const node = bodyRef.current;
3478
3478
  if (!node?.querySelectorAll) return void 0;
@@ -3533,7 +3533,7 @@ function SkillReader({
3533
3533
  { className: "dshsc-reader-title" },
3534
3534
  h("h2", { id: "dshsc-reader-name", tabIndex: -1 }, name || t("viewer.title")),
3535
3535
  row?.status && STATUS_KEY[row.status] ? h("span", { className: `dshsc-tag ${row.status === "current" ? "ok" : "warn"}` }, t(STATUS_KEY[row.status])) : null,
3536
- row?.mode === "off" ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null,
3536
+ row?.enabled === false ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null,
3537
3537
  variant === "drawer" ? h("button", {
3538
3538
  type: "button",
3539
3539
  className: "dshsc-reader-close",
@@ -3542,7 +3542,22 @@ function SkillReader({
3542
3542
  onClick: () => onClose?.()
3543
3543
  }, "×") : null
3544
3544
  ),
3545
- description ? h("p", { className: "dshsc-reader-desc" }, description) : null,
3545
+ description ? h(
3546
+ "div",
3547
+ { className: "dshsc-reader-descblock" },
3548
+ h("p", {
3549
+ className: `dshsc-reader-desc${descOpen ? " expanded" : ""}`,
3550
+ id: "dshsc-reader-desc",
3551
+ ref: descRef
3552
+ }, description),
3553
+ descClamped ? h("button", {
3554
+ type: "button",
3555
+ className: "dshsc-reader-more",
3556
+ "aria-expanded": String(descOpen),
3557
+ "aria-controls": "dshsc-reader-desc",
3558
+ onClick: () => setDescOpen((prev) => !prev)
3559
+ }, t(descOpen ? "action.collapse" : "action.expand")) : null
3560
+ ) : null,
3546
3561
  h(
3547
3562
  "div",
3548
3563
  { className: "dshsc-reader-meta" },
@@ -3757,8 +3772,7 @@ function SkillReader({
3757
3772
  );
3758
3773
  }
3759
3774
  function infoPanel() {
3760
- const agentLabels = (row?.links || []).filter((link) => !link.broken).map((link) => link.target === "dsh-global" ? "DSH" : link.label);
3761
- const projects = row?.scopeProjects || [];
3775
+ const exportLabels = (row?.exports || []).filter((item) => !item.broken).map((item) => item.label);
3762
3776
  const hash = row?.lockHash || row?.currentHash || null;
3763
3777
  const rows = [];
3764
3778
  rows.push(["viewer.info.name", h("span", { className: "mono" }, name), "mono"]);
@@ -3777,9 +3791,9 @@ function SkillReader({
3777
3791
  ["viewer.info.localChange", row?.status === "locally-modified" || row?.status === "conflict" ? t("viewer.info.localChangeYes") : t("viewer.info.localChangeNone")]
3778
3792
  ];
3779
3793
  const linkRows = [
3780
- ["viewer.info.agents", agentLabels.length > 0 ? agentLabels.join("、") : t("meta.unlinked")],
3781
- ["viewer.info.scope", projects.length > 0 ? `${t("viewer.info.scopeProject")}:${projects.map((item) => item.split("/").pop()).join("、")}` : t("viewer.info.scopeGlobal")],
3782
- ["viewer.info.mode", row?.mode === "off" ? t("status.off") : t("mode.on")],
3794
+ ["viewer.info.visibility", row?.visible ? t("meta.modelVisible") : t("meta.modelHidden")],
3795
+ ["viewer.info.exported", exportLabels.length > 0 ? exportLabels.join("、") : t("viewer.info.none")],
3796
+ ["viewer.info.mode", row?.enabled === false ? t("status.off") : t("mode.on")],
3783
3797
  ["viewer.info.status", STATUS_KEY[row?.status] ? t(STATUS_KEY[row.status]) : t("status.unknown")]
3784
3798
  ];
3785
3799
  return h(
@@ -3826,7 +3840,7 @@ function SkillReader({
3826
3840
  onClick: () => void copyTextWithToast(row?.path || "", "viewer.copied")
3827
3841
  }, t("viewer.copyPath")),
3828
3842
  variant === "drawer" && onOpenPage ? h("button", { type: "button", className: "dshsc-btn ghost", onClick: () => onOpenPage() }, t("viewer.openPage")) : null,
3829
- onLinks ? h("button", { type: "button", className: "dshsc-btn", onClick: () => onLinks?.(row) }, t("action.links")) : null,
3843
+ onExports ? h("button", { type: "button", className: "dshsc-btn", onClick: () => onExports?.(row) }, t("action.exports")) : null,
3830
3844
  onUpdate ? h("button", {
3831
3845
  type: "button",
3832
3846
  className: "dshsc-btn tint",
@@ -3835,6 +3849,7 @@ function SkillReader({
3835
3849
  }, t("action.update")) : null
3836
3850
  );
3837
3851
  if (variant === "page") {
3852
+ const tocShown = headings.length > 1;
3838
3853
  return h(
3839
3854
  "div",
3840
3855
  { className: "dshsc-reader-page" },
@@ -3860,8 +3875,8 @@ function SkillReader({
3860
3875
  h("div", { className: "dshsc-reader-hero" }, header),
3861
3876
  h(
3862
3877
  "div",
3863
- { className: "dshsc-reader-layout" },
3864
- headings.length > 1 ? h(
3878
+ { className: `dshsc-reader-layout${tocShown ? " has-toc" : ""}` },
3879
+ tocShown ? h(
3865
3880
  "nav",
3866
3881
  { className: "dshsc-reader-toc", "aria-label": t("viewer.toc") },
3867
3882
  h("div", { className: "dshsc-reader-toc-title" }, t("viewer.toc")),
@@ -3895,19 +3910,19 @@ function SkillReader({
3895
3910
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
3896
3911
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3897
3912
  const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
3898
- const [tab, setTab] = import_react5.default.useState("installed");
3899
- const [view, setView] = import_react5.default.useState("flat");
3900
- const [modal, setModal] = import_react5.default.useState(null);
3901
- const [expanded, setExpanded] = import_react5.default.useState({});
3902
- const [placementBusy, setPlacementBusy] = import_react5.default.useState(null);
3903
- const [placementOverride, setPlacementOverride] = import_react5.default.useState(null);
3904
- const [query, setQuery] = import_react5.default.useState("");
3905
- const [viewer, setViewer] = import_react5.default.useState(null);
3906
- const rootRef = import_react5.default.useRef(null);
3907
- const scrollPos = import_react5.default.useRef(null);
3908
- const restoreScroll = import_react5.default.useRef(null);
3909
- const [filter, setFilter] = import_react5.default.useState("all");
3910
- const [builtinOpen, setBuiltinOpen] = import_react5.default.useState(false);
3913
+ const [tab, setTab] = import_react6.default.useState("installed");
3914
+ const [view, setView] = import_react6.default.useState("flat");
3915
+ const [modal, setModal] = import_react6.default.useState(null);
3916
+ const [expanded, setExpanded] = import_react6.default.useState({});
3917
+ const [placementBusy, setPlacementBusy] = import_react6.default.useState(null);
3918
+ const [placementOverride, setPlacementOverride] = import_react6.default.useState(null);
3919
+ const [query, setQuery] = import_react6.default.useState("");
3920
+ const [viewer, setViewer] = import_react6.default.useState(null);
3921
+ const rootRef = import_react6.default.useRef(null);
3922
+ const scrollPos = import_react6.default.useRef(null);
3923
+ const restoreScroll = import_react6.default.useRef(null);
3924
+ const [filter, setFilter] = import_react6.default.useState("all");
3925
+ const [builtinOpen, setBuiltinOpen] = import_react6.default.useState(false);
3911
3926
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
3912
3927
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
3913
3928
  const rows = data?.installs || [];
@@ -3917,46 +3932,32 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3917
3932
  const updateCount = rows.filter(updatable).length;
3918
3933
  const bulkCount = rows.filter((row) => row.status === "update-available").length;
3919
3934
  const unknownCount = rows.filter((row) => row.status === "unknown").length;
3920
- const offCount = rows.filter((row) => row.mode === "off").length;
3935
+ const offCount = rows.filter((row) => row.enabled === false).length;
3921
3936
  const counts = { all: rows.length, update: updateCount, unknown: unknownCount, off: offCount };
3922
3937
  const queryText = query.trim().toLowerCase();
3923
3938
  const visibleRows = rows.filter((row) => {
3924
3939
  if (filter === "update" && !updatable(row)) return false;
3925
3940
  if (filter === "unknown" && row.status !== "unknown") return false;
3926
- if (filter === "off" && row.mode !== "off") return false;
3941
+ if (filter === "off" && row.enabled !== false) return false;
3927
3942
  if (!queryText) return true;
3928
- const agentLabels = row.links.map((link) => link.target === "dsh-global" ? "DSH" : link.label);
3943
+ const agentLabels = row.exports.map((item) => item.label);
3929
3944
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
3930
3945
  });
3931
- import_react5.default.useEffect(() => {
3946
+ import_react6.default.useEffect(() => {
3932
3947
  if (!viewer || !data) return;
3933
3948
  if (rows.some((row) => row.name === viewer.name)) return;
3934
3949
  setViewer(null);
3935
3950
  setToast({ kind: "err", text: t("viewer.gone") });
3936
3951
  }, [data, rows, t, viewer]);
3937
- const autolinkTargets = (data?.agents?.detected || []).filter((agent) => agent.kind === "agent");
3938
- const autoLinkSelection = data?.settings?.autoLinkTargets;
3939
- const autoLinkAgents = Array.isArray(autoLinkSelection) ? autolinkTargets.filter((agent) => autoLinkSelection.includes(agent.target)) : autolinkTargets;
3940
- const autoLinkDsh = (data?.agents?.all || []).find((agent) => agent.target === "dsh-global") || null;
3941
- const autoLinkLabel = [t("links.dshGlobal"), ...autoLinkAgents.map((agent) => agent.label)].join("、");
3952
+ const exportTargetOptions = (data?.agents?.detected || []).filter((agent) => agent.kind === "agent" && agent.exportable);
3953
+ const exportSelection = data?.settings?.exportTargets;
3954
+ const autoLinkAgents = Array.isArray(exportSelection) ? exportTargetOptions.filter((agent) => exportSelection.includes(agent.target)) : [];
3955
+ const autoLinkLabel = autoLinkAgents.length > 0 ? autoLinkAgents.map((agent) => agent.label).join("、") : t("exportDefault.none");
3942
3956
  const placement = (placementOverride ?? data?.settings?.placement) === "sidebar" ? "sidebar" : "settings";
3943
- function getScrollParent(node) {
3944
- let current = node?.parentElement || null;
3945
- while (current) {
3946
- if (typeof getComputedStyle === "function") {
3947
- const style = getComputedStyle(current);
3948
- if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight) {
3949
- return current;
3950
- }
3951
- }
3952
- current = current.parentElement || null;
3953
- }
3954
- return typeof document !== "undefined" ? document.scrollingElement || null : null;
3955
- }
3956
3957
  function listScroller() {
3957
3958
  const node = rootRef.current?.querySelector?.(".dshsc-listscroll");
3958
- if (node && node.scrollHeight > node.clientHeight) return node;
3959
- return getScrollParent(rootRef.current);
3959
+ if (isScroller(node)) return node;
3960
+ return nearestScroller(rootRef.current);
3960
3961
  }
3961
3962
  function openViewer(row, mode = "drawer") {
3962
3963
  if (!row) return;
@@ -3976,7 +3977,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3976
3977
  if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
3977
3978
  setViewer(null);
3978
3979
  }
3979
- import_react5.default.useLayoutEffect(() => {
3980
+ import_react6.default.useLayoutEffect(() => {
3980
3981
  if (restoreScroll.current === null) return;
3981
3982
  const target = restoreScroll.current;
3982
3983
  restoreScroll.current = null;
@@ -3999,7 +4000,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3999
4000
  }
4000
4001
  async function saveAutoLink(targets) {
4001
4002
  setModal(null);
4002
- await run("settings", () => apiCall("POST", "/settings", { autoLinkTargets: targets }));
4003
+ await run("settings", () => apiCall("POST", "/settings", { exportTargets: targets }));
4003
4004
  }
4004
4005
  const installedActions = {
4005
4006
  onView: (row) => openViewer(row),
@@ -4007,7 +4008,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4007
4008
  onToggleMode: actions.toggleMode,
4008
4009
  onUpdate: actions.requestUpdate,
4009
4010
  onDelete: actions.requestDelete,
4010
- onLinks: (row) => setModal({ type: "links", row }),
4011
+ onExports: (row) => setModal({ type: "exports", row }),
4011
4012
  onCheckUpdates: actions.checkUpdates,
4012
4013
  onUpdateAll: actions.updateAll,
4013
4014
  onAddSkill: actions.requestAddSkill,
@@ -4039,11 +4040,11 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4039
4040
  onClose: closeViewer,
4040
4041
  onOpenPage: viewer.mode === "drawer" ? openPage : null,
4041
4042
  onUpdate: actions.requestUpdate,
4042
- onLinks: (row) => setModal({ type: "links", row }),
4043
+ onExports: (row) => setModal({ type: "exports", row }),
4043
4044
  onToast: (next) => setToast(next)
4044
4045
  }) : null;
4045
4046
  const listNode = h(
4046
- import_react5.default.Fragment,
4047
+ import_react6.default.Fragment,
4047
4048
  null,
4048
4049
  h(Header, {
4049
4050
  t,
@@ -4051,7 +4052,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4051
4052
  plugin: data?.plugin,
4052
4053
  writable,
4053
4054
  autoLinkLabel,
4054
- autoLinkCount: autoLinkAgents.length + 1,
4055
+ autoLinkCount: autoLinkAgents.length,
4055
4056
  tab,
4056
4057
  settingsOpen,
4057
4058
  settingsRef,
@@ -4116,14 +4117,13 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4116
4117
  t,
4117
4118
  modal,
4118
4119
  data,
4119
- autoLinkAgents: autolinkTargets,
4120
- autoLinkDsh,
4120
+ autoLinkAgents: exportTargetOptions,
4121
4121
  pickDirectory,
4122
4122
  actions: {
4123
4123
  probeSkills: actions.probeSkills,
4124
4124
  installSkills: actions.installSkills,
4125
4125
  addSubscription: actions.addSubscription,
4126
- saveLinks: actions.saveLinks
4126
+ saveExports: actions.saveExports
4127
4127
  },
4128
4128
  onClose: () => setModal(null),
4129
4129
  onSaveAutoLink: saveAutoLink