@morehao/dsh-skills-console 0.2.0 → 0.2.1

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
@@ -1,1246 +1,2378 @@
1
- /**
2
- * Client half:设置页「技能控制台」分区。
3
- * 一页两 tab:已装 / 订阅。行内操作列 = 开关 + 更新 + 接入设置 + 删除。
4
- * 所有数据来自 Host 的 /api/morehao/skills-console/*,这里只做展示与提交。
5
- */
1
+ /* AUTO-GENERATED from client/ by scripts/build-client.mjs — do not edit. */
2
+ /* eslint-disable */
6
3
  window.__ModuleLoader__.load({
7
- id: '@morehao/dsh-skills-console',
4
+ id: "@morehao/dsh-skills-console",
8
5
  factory(require) {
9
- const React = require('react');
10
- const h = React.createElement;
11
- const API = '/api/morehao/skills-console';
12
-
13
- const DICT = {
14
- zh: {
15
- 'section.title': '技能控制台',
16
- 'page.title': '技能控制台',
17
- 'section.subtitle': 'canonical 只由 skills CLI 写入;这里管装、更新、删除与接入。',
18
- 'tab.installed': '已装',
19
- 'tab.subscriptions': '订阅',
20
- 'action.addSkill': '添加技能',
21
- 'action.addSubscription': '添加订阅',
22
- 'action.refresh': '刷新',
23
- 'action.update': '更新',
24
- 'action.updateAll': '全部更新',
25
- 'action.delete': '删除',
26
- 'action.links': '接入设置',
27
- 'action.install': '安装',
28
- 'action.adopt': '接管',
29
- 'action.save': '保存',
30
- 'action.cancel': '取消',
31
- 'action.choose': '选择目录',
32
- 'action.retry': '重新加载',
33
- 'view.flat': '平铺',
34
- 'view.agent': '按 Agent',
35
- autoLink: '自动链接',
36
- 'placement.label': '入口位置',
37
- 'placement.settings': '设置里',
38
- 'placement.sidebar': '侧边栏',
39
- 'placement.hint': '切换后立即生效;移到设置后从侧栏底部「设置 → 技能」打开',
40
- 'github.open': '打开 GitHub 仓库',
41
- 'status.current': '最新',
42
- 'status.update': '有新版本',
43
- 'status.local': '本地已改',
44
- 'status.conflict': '本地已改 + 上游已变',
45
- 'status.deleted': '上游已移除',
46
- 'status.broken': '异常:链接断裂',
47
- 'status.unmanaged': '未纳入追踪',
48
- 'status.unknown': '待检查',
49
- 'adopt.switchHint': '还未纳入追踪,不能启停',
50
- 'adopt.noUpstream': '没有远端来源,暂时无法检测更新',
51
- 'toast.checked': '检查完成',
52
- 'mode.on': '启用',
53
- 'mode.off': '停用',
54
- 'mode.offHint': '停用中:DSH 看不到它,但已链接的其他 Agent 照常可用',
55
- 'links.dshGlobal': 'dsh(全局)',
56
- 'links.required': '必选',
57
- 'links.scope': '在 DSH 里的作用范围',
58
- 'links.scopeGlobal': '全局(默认)',
59
- 'links.scopeProjects': '仅以下项目',
60
- 'links.targets': '链接到哪些 Agent',
61
- 'links.detectedHint': '只显示本机已安装(目录存在)的 Agent;已删除的 Agent 会从列表消失,旧链接记录保留但不展示。',
62
- 'links.workspaces': 'DSH 工作区',
63
- 'links.noWorkspaces': '还没有 DSH 工作区,可以点下面「选择其他目录」',
64
- 'links.pickDirectory': '选择其他目录…',
65
- 'links.selected': '其他已选目录',
66
- 'links.hint': '保存后按勾选建/删软链;canonical 里的技能真身不会移动。',
67
- 'autoLink.detected': '安装后自动接入',
68
- 'autoLink.none': '本机没有检测到其他 Agent',
69
- 'autoLink.hint': 'dsh(全局)始终接入;这里只影响之后安装时自动建链,已装的技能请在「接入设置」里改。',
70
- 'empty.skills': '还没有安装任何技能',
71
- 'empty.skillsHint': '在终端跑 `npx skills add owner/repo`,或点右上角「添加技能」',
72
- 'empty.subs': '还没有添加订阅',
73
- 'empty.subsHint': '订阅只是关注,不安装任何技能',
74
- 'banner.cli': 'skills CLI 不可用或不支持 --json/--full-depth,写操作已禁用',
75
- 'banner.readonly': '状态文件损坏,插件处于只读模式',
76
- 'banner.visible': '模型可见技能与本地账本不一致,差异已列在下方',
77
- 'form.source': '来源(owner/repo 或 GitHub URL)',
78
- 'form.sourceHint': '例如 morehao/ai-kit',
79
- 'form.skills': '技能名(多个用逗号分隔)',
80
- 'form.ref': 'ref(分支 / tag,可空)',
81
- 'form.subpath': '子目录(可空)',
82
- 'form.hint': '会立即下载并写入 ~/.agents/skills,并按「自动链接」设置接入 Agent。',
83
- 'form.subHint': '只记录你关注这个仓库,不会安装任何技能。',
84
- 'confirm.deleteTitle': '删除技能',
85
- 'confirm.deleteBody': '会从本机移除,断开本插件建的链接并调 skills CLI 卸载;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。',
86
- 'confirm.updateTitle': '本地有改动',
87
- 'confirm.updateBody': '更新会覆盖这些本地改动;旧内容已备份,可回滚。',
88
- 'error.conflictTitle': '删除被拦截',
89
- 'busy.working': '处理中…',
90
- 'meta.from': '来自',
91
- 'meta.available': '可用',
92
- 'meta.unlinked': '未接入任何 Agent',
93
- 'meta.scope': '范围',
94
- 'meta.none': '无',
95
- installed: '已装',
96
- notInstalled: '未装',
97
- skillCount: '个技能',
98
- visibleOnly: '模型可见但不在 canonical',
99
- visibleOnlyOther: 'DSH 能看到、但不在 ~/.agents/skills',
100
- visibleOnlyBuiltin: 'DSH 自带技能(本插件不管理)',
101
- external: '非本插件创建',
102
- },
103
- en: {
104
- 'section.title': 'Skills Console',
105
- 'page.title': 'Skills Console',
106
- 'section.subtitle': 'The skills CLI owns canonical; this page installs, updates, removes and links.',
107
- 'tab.installed': 'Installed',
108
- 'tab.subscriptions': 'Subscriptions',
109
- 'action.addSkill': 'Add skill',
110
- 'action.addSubscription': 'Add subscription',
111
- 'action.refresh': 'Refresh',
112
- 'action.update': 'Update',
113
- 'action.updateAll': 'Update all',
114
- 'action.delete': 'Delete',
115
- 'action.links': 'Link settings',
116
- 'action.install': 'Install',
117
- 'action.adopt': 'Adopt',
118
- 'action.save': 'Save',
119
- 'action.cancel': 'Cancel',
120
- 'action.choose': 'Choose folder',
121
- 'action.retry': 'Reload',
122
- 'view.flat': 'Flat',
123
- 'view.agent': 'By agent',
124
- 'autoLink': 'Auto-link',
125
- 'status.current': 'Up to date',
126
- 'status.update': 'Update available',
127
- 'status.local': 'Locally modified',
128
- 'status.conflict': 'Local + upstream changed',
129
- 'status.deleted': 'Removed upstream',
130
- 'status.broken': 'Broken link',
131
- 'status.unmanaged': 'Untracked',
132
- 'status.unknown': 'Not checked',
133
- 'adopt.switchHint': 'Not tracked yet, cannot be enabled or disabled',
134
- 'adopt.noUpstream': 'No upstream source; update checks are unavailable',
135
- 'toast.checked': 'Checked',
136
- 'mode.on': 'Enabled',
137
- 'mode.off': 'Disabled',
138
- 'mode.offHint': 'Disabled: DSH cannot see it, other linked agents still can',
139
- 'links.dshGlobal': 'dsh (global)',
140
- 'links.required': 'required',
141
- 'links.scope': 'Scope inside DSH',
142
- 'links.scopeGlobal': 'Global (default)',
143
- 'links.scopeProjects': 'Only these projects',
144
- 'links.targets': 'Link to agents',
145
- 'links.detectedHint': 'Only installed agents are listed; deleted agents disappear but their old link records are kept and hidden.',
146
- 'links.workspaces': 'DSH workspaces',
147
- 'links.noWorkspaces': 'No DSH workspaces yet; use "Choose another folder" below',
148
- 'links.pickDirectory': 'Choose another folder…',
149
- 'links.selected': 'Other selected folders',
150
- 'links.hint': 'Saving creates/removes only symlinks; the skill itself stays in canonical.',
151
- 'autoLink.detected': 'Auto-link after install',
152
- 'autoLink.none': 'No other agents detected on this machine',
153
- 'autoLink.hint': 'dsh (global) is always linked; this only affects future installs. Change existing skills under "Link settings".',
154
- 'empty.skills': 'No skills installed yet',
155
- 'empty.skillsHint': 'Run `npx skills add owner/repo` or use "Add skill".',
156
- 'empty.subs': 'No subscriptions yet',
157
- 'empty.subsHint': 'A subscription only tracks a repository; it installs nothing.',
158
- 'banner.cli': 'skills CLI unavailable or lacks --json/--full-depth; write actions disabled',
159
- 'banner.readonly': 'State file is corrupt; the plugin is read-only',
160
- 'banner.visible': 'Model-visible skills differ from the local ledger',
161
- 'form.source': 'Source (owner/repo or GitHub URL)',
162
- 'form.sourceHint': 'e.g. morehao/ai-kit',
163
- 'form.skills': 'Skill names (comma separated)',
164
- 'form.ref': 'ref (branch / tag, optional)',
165
- 'form.subpath': 'Subdirectory (optional)',
166
- 'form.hint': 'Downloads into ~/.agents/skills and links agents per the auto-link setting.',
167
- 'form.subHint': 'Only records the repository; installs nothing.',
168
- 'confirm.deleteTitle': 'Delete skill',
169
- '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.',
170
- 'confirm.updateTitle': 'Local changes',
171
- 'confirm.updateBody': 'Updating overwrites local changes; the old content is backed up and can be rolled back.',
172
- 'error.conflictTitle': 'Delete blocked',
173
- 'busy.working': 'Working…',
174
- 'meta.from': 'from',
175
- 'meta.available': 'available',
176
- 'meta.unlinked': 'not linked to any agent',
177
- 'meta.scope': 'scope',
178
- 'meta.none': 'none',
179
- installed: 'installed',
180
- notInstalled: 'not installed',
181
- skillCount: 'skills',
182
- visibleOnly: 'Visible to the model but not in canonical',
183
- visibleOnlyOther: 'DSH can see these but they are not in ~/.agents/skills',
184
- visibleOnlyBuiltin: 'Shipped with DSH (this plugin does not manage them)',
185
- external: 'created outside this plugin',
186
- 'adopt.done': 'now tracked',
187
- },
188
- };
189
-
190
- const CSS = `
191
- /* 主面板形态:与 DSH 自带管理页同一边距/滚动方式 */
192
- .dshsc-page { box-sizing: border-box; width: 100%; height: 100%; overflow: auto;
193
- padding: calc(28px + var(--dsh-frame-top-clearance, 0px)) clamp(24px, 4vw, 48px) 48px; }
194
- .dshsc-page .dshsc-root { max-width: 980px; margin: 0 auto; }
195
- .dshsc-page .dshsc-title { font-size: 20px; font-weight: 500; line-height: 28px; }
196
- .dshsc-page .dshsc-sub { font-size: 13px; line-height: 20px; }
197
- .dshsc-root { display: flex; flex-direction: column; gap: 16px; color: var(--dsw-alias-label-primary); }
198
- .dshsc-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
199
- .dshsc-heading { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
200
- .dshsc-heading-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
201
- .dshsc-logo { flex: none; display: block; border-radius: 8px; }
202
- .dshsc-title { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; }
203
- .dshsc-sub { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin: 0; max-width: 64ch; }
204
- .dshsc-spacer { flex: 1; }
205
- .dshsc-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
206
- .dshsc-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; }
207
- .dshsc-prefbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px;
208
- color: var(--dsw-alias-label-secondary); }
209
- .dshsc-pref-hint { color: var(--dsw-alias-label-tertiary); }
210
-
211
- .dshsc-btn { font: inherit; font-size: 12px; line-height: 1; padding: 7px 11px; border-radius: 8px; cursor: pointer;
212
- border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary);
213
- transition: background .12s ease, border-color .12s ease, color .12s ease; }
214
- .dshsc-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
215
- .dshsc-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
216
- .dshsc-btn:disabled { opacity: .42; cursor: not-allowed; }
217
- /* brand-primary 与 bg-base 在明暗主题里互为反色:文字用 bg-base 才不会是白底白字 */
218
- .dshsc-btn.primary { background: var(--dsw-alias-brand-primary); border-color: transparent; color: var(--dsw-alias-bg-base); font-weight: 600; }
219
- .dshsc-btn.primary:hover:not(:disabled) { background: var(--dsw-alias-brand-primary); opacity: .9; }
220
- .dshsc-btn.ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); padding: 6px 8px; }
221
- .dshsc-btn.ghost:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
222
- .dshsc-btn.ghost.danger { color: var(--dsw-alias-state-error-primary); }
223
- .dshsc-btn.ghost.danger:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); }
224
-
225
- .dshsc-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
226
- .dshsc-tabs { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--dsw-alias-border-l1);
227
- border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
228
- .dshsc-tab { font: inherit; font-size: 12px; padding: 6px 11px; border: 0; border-radius: 7px; background: transparent;
229
- cursor: pointer; color: var(--dsw-alias-label-secondary); }
230
- .dshsc-tab:hover { color: var(--dsw-alias-label-primary); }
231
- .dshsc-tab.active { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary);
232
- box-shadow: 0 0 0 1px var(--dsw-alias-border-l1), 0 1px 2px rgba(0,0,0,.12); }
233
-
234
- .dshsc-segment { display: inline-flex; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
235
- background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
236
- .dshsc-segment .dshsc-btn { border: 0; border-radius: 0; background: transparent; padding: 6px 10px; }
237
- .dshsc-segment .dshsc-btn + .dshsc-btn { border-left: 1px solid var(--dsw-alias-border-l1); }
238
- .dshsc-segment .dshsc-btn[aria-pressed="true"] { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
239
-
240
- .dshsc-list { display: flex; flex-direction: column; gap: 8px; }
241
- .dshsc-row { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1);
242
- padding: 12px 14px; transition: border-color .12s ease, background .12s ease; }
243
- .dshsc-row:hover { border-color: var(--dsw-alias-border-l2); }
244
- .dshsc-row.off { opacity: .62; }
245
- .dshsc-row-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
246
- .dshsc-name { font-size: 13px; font-weight: 600; letter-spacing: -.005em; }
247
- .dshsc-status { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; line-height: 1; padding: 4px 8px;
248
- border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2);
249
- color: var(--dsw-alias-label-secondary); white-space: nowrap; }
250
- .dshsc-status::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: .85; }
251
- .dshsc-status.ok { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 13%, transparent); }
252
- .dshsc-status.warn { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 13%, transparent); }
253
- .dshsc-status.err { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 13%, transparent); }
254
- .dshsc-meta { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin-top: 6px; display: flex; flex-wrap: wrap; }
255
- .dshsc-row .dshsc-meta span + span::before { content: '·'; margin: 0 7px; color: var(--dsw-alias-label-tertiary); }
256
- .dshsc-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; flex-wrap: wrap; }
257
-
258
- /* 与 DSH 自带开关同尺寸/同 token:关是 border-l3 轨道,开是 brand-primary 轨道 */
259
- .dshsc-switch { box-sizing: border-box; position: relative; flex: none; width: 36px; height: 20px; padding: 2px;
260
- border: 0; border-radius: 999px; background: var(--dsw-alias-border-l3); cursor: pointer;
261
- transition: background .14s ease; }
262
- .dshsc-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
263
- .dshsc-switch:disabled { cursor: not-allowed; opacity: .5; }
264
- .dshsc-switch[aria-checked="true"] { background: var(--dsw-alias-brand-primary); }
265
- .dshsc-knob { display: block; width: 16px; height: 16px; border-radius: 50%;
266
- background: var(--dsw-alias-switch-thumb); transition: transform .12s ease, background .12s ease; }
267
- .dshsc-switch[aria-checked="true"] .dshsc-knob { transform: translateX(16px); background: var(--dsw-alias-label-primary-foreground); }
268
-
269
- .dshsc-banner { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 9px 12px; font-size: 12px;
270
- line-height: 1.55; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary);
271
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
272
- .dshsc-banner.err { color: var(--dsw-alias-state-error-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, var(--dsw-alias-border-l1)); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
273
- .dshsc-banner.warn { color: var(--dsw-alias-state-warn-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, var(--dsw-alias-border-l1)); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent); }
274
-
275
- .dshsc-empty { text-align: center; padding: 34px 16px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 12px;
276
- background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 55%, transparent); }
277
- .dshsc-empty .dshsc-name { font-size: 14px; }
278
- .dshsc-empty .dshsc-meta { justify-content: center; margin-top: 6px; }
279
- .dshsc-group { font-size: 11px; font-weight: 600; letter-spacing: .04em; color: var(--dsw-alias-label-tertiary); margin: 12px 0 6px; }
280
-
281
- .dshsc-overlay { position: fixed; inset: 0; background: rgba(4,6,9,.55); display: flex; align-items: center; justify-content: center;
282
- z-index: 60; padding: 20px; backdrop-filter: blur(2px); }
283
- .dshsc-modal { position: relative; width: min(600px, 100%); max-height: calc(100vh - 40px); overflow: auto;
284
- background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 14px;
285
- padding: 18px; display: flex; flex-direction: column; gap: 14px;
286
- box-shadow: 0 20px 56px rgba(0,0,0,.38), 0 2px 8px rgba(0,0,0,.18); }
287
- .dshsc-modal h3 { margin: 0; font-size: 15px; font-weight: 600; padding-right: 26px; }
288
- .dshsc-modal-close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border: 0; border-radius: 7px;
289
- background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; cursor: pointer; }
290
- .dshsc-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
291
-
292
- .dshsc-field { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
293
- .dshsc-field input { font: inherit; font-size: 13px; padding: 8px 10px; border-radius: 8px;
294
- border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); outline: none; }
295
- .dshsc-field input:focus { border-color: var(--dsw-alias-brand-primary);
296
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
297
-
298
- .dshsc-check { display: flex; align-items: center; gap: 9px; font-size: 13px; padding: 6px 8px; border-radius: 8px; cursor: pointer; }
299
- .dshsc-check:hover { background: var(--dsw-alias-bg-layer-1); }
300
- .dshsc-check input { width: 14px; height: 14px; accent-color: var(--dsw-alias-brand-primary); flex: none; }
301
- .dshsc-check small { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
302
- .dshsc-check span { white-space: nowrap; }
303
-
304
- .dshsc-panel { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1);
305
- padding: 4px; max-height: 240px; overflow: auto; display: flex; flex-direction: column; }
306
- .dshsc-panel .dshsc-check:hover { background: var(--dsw-alias-bg-layer-2); }
307
-
308
- .dshsc-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
309
- .dshsc-toast { position: fixed; right: 18px; bottom: 18px; z-index: 70; font-size: 12px; padding: 10px 13px; border-radius: 9px;
310
- background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2);
311
- box-shadow: 0 8px 24px rgba(0,0,0,.22); max-width: 420px; }
312
- .dshsc-toast.err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
313
- .dshsc-toast.ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 50%, var(--dsw-alias-border-l2)); }
314
- .dshsc-substat { font-size: 12px; color: var(--dsw-alias-label-secondary); }
315
- .dshsc-sub-entry { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 7px 8px 7px 14px; border-top: 1px solid var(--dsw-alias-border-l1); }
316
- .dshsc-sub-entry:first-child { border-top: 0; }
317
- .dshsc-sub-list { margin-top: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
318
- .dshsc-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
319
- .dshsc-chevron { width: 26px; padding: 4px 0; text-align: center; }
320
- `;
321
-
322
- function short(sha) {
323
- return sha ? String(sha).slice(0, 7) : '';
6
+ var module = { exports: {} };
7
+ var exports = module.exports;
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
27
+ // If the importer is in node compatibility mode or this is not an ESM
28
+ // file that has been converted to a CommonJS file using a Babel-
29
+ // compatible transform (i.e. "__esModule" has not been set), then set
30
+ // "default" to the CommonJS "module.exports" for node compatibility.
31
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
32
+ mod
33
+ ));
34
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
+
36
+ // client/index.js
37
+ var index_exports = {};
38
+ __export(index_exports, {
39
+ apply: () => apply,
40
+ inject: () => inject
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // client/api.js
45
+ var API = "/api/morehao/skills-console";
46
+ function short(sha) {
47
+ return sha ? String(sha).slice(0, 7) : "";
48
+ }
49
+ async function apiCall(method, path, body) {
50
+ const response = await fetch(`${API}${path}`, {
51
+ method,
52
+ headers: { "content-type": "application/json", "x-dsh-skills-console": "1" },
53
+ body: body === void 0 ? void 0 : JSON.stringify(body),
54
+ cache: "no-store"
55
+ });
56
+ let json = {};
57
+ try {
58
+ json = await response.json();
59
+ } catch {
60
+ json = {};
61
+ }
62
+ if (!response.ok || json.ok === false) {
63
+ const error = new Error(json.message || `HTTP ${response.status}`);
64
+ error.code = json.code;
65
+ error.detail = json.detail;
66
+ throw error;
67
+ }
68
+ return json;
69
+ }
70
+ var GITHUB_URL = "https://github.com/morehao/dsh-skills-console";
71
+
72
+ // client/copy.js
73
+ var DICT = {
74
+ zh: {
75
+ "section.title": "技能控制台",
76
+ "page.title": "技能控制台",
77
+ "section.subtitle": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。",
78
+ "tab.installed": "已安装",
79
+ "tab.subscriptions": "订阅",
80
+ "action.addSkill": "添加技能",
81
+ "action.addSubscription": "添加订阅",
82
+ "action.checkUpdates": "检查更新",
83
+ "action.update": "更新",
84
+ "action.updateAll": "全部更新",
85
+ "action.delete": "删除",
86
+ "action.unsubscribe": "取消订阅",
87
+ "action.view": "查看",
88
+ "action.hide": "收起",
89
+ "action.links": "接入设置",
90
+ "action.install": "安装",
91
+ "action.expand": "展开",
92
+ "action.collapse": "收起",
93
+ "action.adopt": "接管",
94
+ "action.save": "保存",
95
+ "action.cancel": "取消",
96
+ "action.choose": "选择目录",
97
+ "action.retry": "重新加载",
98
+ "view.flat": "平铺",
99
+ "view.agent": "按 Agent",
100
+ autoLink: "自动接入",
101
+ "settings.title": "插件设置",
102
+ "version.label": "当前版本",
103
+ "placement.label": "侧边栏入口",
104
+ "placement.effect": "切换后立即生效",
105
+ "placement.on": "已开启",
106
+ "placement.off": "已关闭",
107
+ "placement.hint": "开启后侧栏出现「技能控制台」入口;关闭则只在「设置 → 技能」显示。",
108
+ "github.open": "打开 GitHub 仓库",
109
+ "status.current": "最新",
110
+ "status.update": "可更新",
111
+ "status.local": "本地已改",
112
+ "status.conflict": "本地已改 + 上游已变",
113
+ "status.deleted": "上游已移除",
114
+ "status.broken": "异常:软链断裂",
115
+ "status.unmanaged": "未纳入追踪",
116
+ "status.unknown": "待检查",
117
+ "status.off": "已停用",
118
+ "status.brokenHint": "软链目标不存在:重新接入,或删除后重装",
119
+ "adopt.switchHint": "还未纳入追踪,不能启停",
120
+ "adopt.noUpstream": "没有远端来源,暂时无法检测更新",
121
+ "adopt.done": "已纳入追踪",
122
+ "toast.checked": "检查完成",
123
+ "mode.on": "启用",
124
+ "mode.off": "停用",
125
+ "mode.offHint": "停用中:DSH 看不到它,但已接入的其他 Agent 照常可用",
126
+ "links.dshGlobal": "dsh(全局)",
127
+ "links.required": "必选",
128
+ "links.scope": "在 DSH 里的作用范围",
129
+ "links.scopeGlobal": "全局(默认)",
130
+ "links.scopeProjects": "仅以下项目",
131
+ "links.targets": "接入到哪些 Agent",
132
+ "links.detectedHint": "只显示本机已安装(目录存在)的 Agent;已删除的会从列表消失,旧接入记录保留但不展示。",
133
+ "links.canonical": "技能本体:{path};下面列的是各 Agent 的接入目录(软链目标)。",
134
+ "links.workspaces": "DSH 工作区",
135
+ "links.noWorkspaces": "还没有 DSH 工作区,可以点下面「选择其他目录」",
136
+ "links.pickDirectory": "选择其他目录…",
137
+ "links.selected": "其他已选目录",
138
+ "links.hint": "保存后按勾选建立或移除接入;技能本体保留在原位置,不会移动。",
139
+ "autoLink.detected": "安装后自动接入",
140
+ "autoLink.none": "本机没有检测到其他 Agent",
141
+ "autoLink.hint": "dsh(全局)始终接入;这里只影响之后安装时的自动接入,已安装的技能请在「接入设置」里改。",
142
+ "empty.skills": "还没有安装任何技能",
143
+ "empty.skillsHint": "在终端跑 `npx skills add owner/repo`,或点右上角「添加技能」",
144
+ "empty.subs": "还没有添加订阅",
145
+ "empty.subsHint": "订阅只是关注,不安装任何技能",
146
+ "banner.cli": "本地技能工具不可用,暂时无法安装或修改",
147
+ "banner.readonly": "状态文件损坏,插件处于只读模式",
148
+ "banner.visible": "模型可见技能与本地账本不一致,差异已列在下方",
149
+ "form.source": "来源(owner/repo 或 GitHub URL)",
150
+ "form.sourceHint": "例如 morehao/ai-kit",
151
+ "form.skills": "技能名(多个用逗号分隔)",
152
+ "form.ref": "ref(分支 / tag,可空)",
153
+ "form.subpath": "子目录(可空)",
154
+ "form.hint": "会立即下载并写入 ~/.agents/skills,并按「自动接入」设置接到对应 Agent。",
155
+ "form.subHint": "只记录你关注这个仓库,不会安装任何技能。",
156
+ "confirm.deleteTitle": "删除技能",
157
+ "confirm.deleteBody": "会从本机移除,断开本插件建立的软链并调 skills CLI 卸载;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
158
+ "confirm.updateTitle": "本地有改动",
159
+ "confirm.updateBody": "更新会覆盖这些本地改动;旧内容已备份,可回滚。",
160
+ "error.conflictTitle": "删除被拦截",
161
+ "busy.working": "处理中…",
162
+ "meta.from": "来自",
163
+ "meta.noUpstream": "无远端来源 · 不检测更新",
164
+ "meta.unlinked": "未接入任何 Agent",
165
+ "meta.scope": "范围",
166
+ "meta.onlyProject": "仅",
167
+ "meta.agentsAll": "已接入的 Agent",
168
+ "meta.none": "无",
169
+ "filter.all": "全部",
170
+ "filter.update": "可更新",
171
+ "filter.unknown": "待检查",
172
+ "filter.off": "已停用",
173
+ "search.placeholder": "搜索技能或 Agent",
174
+ "search.clear": "清除",
175
+ "empty.loading": "正在读取技能清单…",
176
+ "empty.filtered": "没有匹配的技能",
177
+ "empty.filteredHint": "换个关键词,或清除筛选条件",
178
+ "hint.unknown": "还有 {n} 个技能未检查更新。",
179
+ "group.dshGlobal": "DSH · 全局",
180
+ "group.dshProject": "DSH · 仅特定项目",
181
+ "group.off": "已停用",
182
+ "group.unlinked": "未接入 Agent",
183
+ installed: "已安装",
184
+ notInstalled: "未安装",
185
+ skillCount: "个技能",
186
+ visibleOnly: "模型可见,但不在本地技能库",
187
+ visibleOnlyOther: "DSH 能看到,但不在 ~/.agents/skills",
188
+ visibleOnlyBuiltin: "本插件仅管理 ~/.agents/skills 下的技能,DSH 内置技能不在管理范围内",
189
+ "builtin.view": "查看内置技能({n})",
190
+ "builtin.hide": "收起内置技能",
191
+ external: "非本插件创建"
192
+ },
193
+ en: {
194
+ "section.title": "Skills Console",
195
+ "page.title": "Skills Console",
196
+ "section.subtitle": "Manage local skills in one place: install, update, remove and link — so the same skill can be reused across agents.",
197
+ "tab.installed": "Installed",
198
+ "tab.subscriptions": "Subscriptions",
199
+ "action.addSkill": "Add skill",
200
+ "action.addSubscription": "Add subscription",
201
+ "action.checkUpdates": "Check for updates",
202
+ "action.update": "Update",
203
+ "action.updateAll": "Update all",
204
+ "action.delete": "Delete",
205
+ "action.unsubscribe": "Unsubscribe",
206
+ "action.view": "View",
207
+ "action.hide": "Hide",
208
+ "action.links": "Link settings",
209
+ "action.install": "Install",
210
+ "action.expand": "Expand",
211
+ "action.collapse": "Collapse",
212
+ "action.adopt": "Adopt",
213
+ "action.save": "Save",
214
+ "action.cancel": "Cancel",
215
+ "action.choose": "Choose folder",
216
+ "action.retry": "Reload",
217
+ "view.flat": "Flat",
218
+ "view.agent": "By agent",
219
+ "autoLink": "Auto-link",
220
+ "settings.title": "Plugin settings",
221
+ "version.label": "Version",
222
+ "placement.label": "Sidebar entry",
223
+ "placement.effect": "Applies immediately",
224
+ "placement.on": "on",
225
+ "placement.off": "off",
226
+ "placement.hint": "On: a Skills Console entry appears in the sidebar. Off: it only shows under Settings → Skills.",
227
+ "github.open": "Open GitHub repository",
228
+ "status.current": "Up to date",
229
+ "status.update": "Update available",
230
+ "status.local": "Locally modified",
231
+ "status.conflict": "Local + upstream changed",
232
+ "status.deleted": "Removed upstream",
233
+ "status.broken": "Broken link",
234
+ "status.unmanaged": "Untracked",
235
+ "status.unknown": "Not checked",
236
+ "status.off": "Disabled",
237
+ "status.brokenHint": "The link target is missing: relink, or delete and reinstall",
238
+ "adopt.switchHint": "Not tracked yet, cannot be enabled or disabled",
239
+ "adopt.noUpstream": "No upstream source; update checks are unavailable",
240
+ "toast.checked": "Checked",
241
+ "mode.on": "Enabled",
242
+ "mode.off": "Disabled",
243
+ "mode.offHint": "Disabled: DSH cannot see it, other linked agents still can",
244
+ "links.dshGlobal": "dsh (global)",
245
+ "links.required": "required",
246
+ "links.scope": "Scope inside DSH",
247
+ "links.scopeGlobal": "Global (default)",
248
+ "links.scopeProjects": "Only these projects",
249
+ "links.targets": "Link to agents",
250
+ "links.detectedHint": "Only installed agents are listed; deleted agents disappear but their old link records are kept and hidden.",
251
+ "links.canonical": "Skill source: {path}; the entries below are each agent's link target.",
252
+ "links.workspaces": "DSH workspaces",
253
+ "links.noWorkspaces": 'No DSH workspaces yet; use "Choose another folder" below',
254
+ "links.pickDirectory": "Choose another folder…",
255
+ "links.selected": "Other selected folders",
256
+ "links.hint": "Saving only adds or removes links; the skill itself stays where it is.",
257
+ "autoLink.detected": "Auto-link after install",
258
+ "autoLink.none": "No other agents detected on this machine",
259
+ "autoLink.hint": 'dsh (global) is always linked; this only affects future installs. Change existing skills under "Link settings".',
260
+ "empty.skills": "No skills installed yet",
261
+ "empty.skillsHint": 'Run `npx skills add owner/repo` or use "Add skill".',
262
+ "empty.subs": "No subscriptions yet",
263
+ "empty.subsHint": "A subscription only tracks a repository; it installs nothing.",
264
+ "banner.cli": "The local skills tool is unavailable; install and changes are disabled",
265
+ "banner.readonly": "State file is corrupt; the plugin is read-only",
266
+ "banner.visible": "Model-visible skills differ from the local ledger",
267
+ "form.source": "Source (owner/repo or GitHub URL)",
268
+ "form.sourceHint": "e.g. morehao/ai-kit",
269
+ "form.skills": "Skill names (comma separated)",
270
+ "form.ref": "ref (branch / tag, optional)",
271
+ "form.subpath": "Subdirectory (optional)",
272
+ "form.hint": "Downloads into ~/.agents/skills and links agents per the auto-link setting.",
273
+ "form.subHint": "Only records the repository; installs nothing.",
274
+ "confirm.deleteTitle": "Delete skill",
275
+ "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.",
276
+ "confirm.updateTitle": "Local changes",
277
+ "confirm.updateBody": "Updating overwrites local changes; the old content is backed up and can be rolled back.",
278
+ "error.conflictTitle": "Delete blocked",
279
+ "busy.working": "Working…",
280
+ "meta.from": "from",
281
+ "meta.noUpstream": "no upstream · updates not checked",
282
+ "meta.unlinked": "not linked to any agent",
283
+ "meta.scope": "scope",
284
+ "meta.onlyProject": "only",
285
+ "meta.agentsAll": "Connected agents",
286
+ "meta.none": "none",
287
+ "filter.all": "All",
288
+ "filter.update": "Updates",
289
+ "filter.unknown": "Not checked",
290
+ "filter.off": "Disabled",
291
+ "search.placeholder": "Search skills or agents",
292
+ "search.clear": "Clear",
293
+ "empty.loading": "Loading skills…",
294
+ "empty.filtered": "No matching skills",
295
+ "empty.filteredHint": "Try another keyword, or clear the filters",
296
+ "hint.unknown": "{n} skills have not been checked for updates.",
297
+ "group.dshGlobal": "DSH · global",
298
+ "group.dshProject": "DSH · project only",
299
+ "group.off": "Disabled",
300
+ "group.unlinked": "Not linked",
301
+ installed: "installed",
302
+ notInstalled: "not installed",
303
+ skillCount: "skills",
304
+ visibleOnly: "Visible to the model but not in the local skill library",
305
+ visibleOnlyOther: "DSH can see these, but they are not under ~/.agents/skills",
306
+ visibleOnlyBuiltin: "This plugin only manages skills under ~/.agents/skills; DSH built-in skills are out of scope",
307
+ "builtin.view": "View built-ins ({n})",
308
+ "builtin.hide": "Hide built-ins",
309
+ external: "created outside this plugin",
310
+ "adopt.done": "now tracked"
311
+ }
312
+ };
313
+ function buildTranslate(locale) {
314
+ const active = () => {
315
+ try {
316
+ return locale?.getLocale?.().active || "zh";
317
+ } catch {
318
+ return "zh";
324
319
  }
325
-
326
- async function apiCall(method, path, body) {
327
- const response = await fetch(`${API}${path}`, {
328
- method,
329
- headers: { 'content-type': 'application/json', 'x-dsh-skills-console': '1' },
330
- body: body === undefined ? undefined : JSON.stringify(body),
331
- cache: 'no-store',
320
+ };
321
+ return (key) => {
322
+ const id = active();
323
+ const pack = DICT[id] || DICT.zh;
324
+ return pack[key] ?? DICT.zh[key] ?? key;
325
+ };
326
+ }
327
+
328
+ // client/host.js
329
+ var import_react = __toESM(require("react"), 1);
330
+ var h = import_react.default.createElement;
331
+ var ReactDOM = null;
332
+ try {
333
+ ReactDOM = require("react-dom");
334
+ } catch {
335
+ ReactDOM = null;
336
+ }
337
+ function portalize(node) {
338
+ return ReactDOM?.createPortal && typeof document !== "undefined" && document.body ? ReactDOM.createPortal(node, document.body) : node;
339
+ }
340
+
341
+ // client/icons.js
342
+ function Logo({ size = 30 }) {
343
+ return h(
344
+ "svg",
345
+ {
346
+ className: "dshsc-logo",
347
+ width: size,
348
+ height: size,
349
+ viewBox: "0 0 64 64",
350
+ "aria-hidden": true
351
+ },
352
+ h(
353
+ "defs",
354
+ null,
355
+ h(
356
+ "linearGradient",
357
+ { id: "dshsc-logo-bg", x1: 6, y1: 2, x2: 58, y2: 62, gradientUnits: "userSpaceOnUse" },
358
+ h("stop", { stopColor: "#3b82f6" }),
359
+ h("stop", { offset: 1, stopColor: "#1d4ed8" })
360
+ )
361
+ ),
362
+ h("rect", { x: 3, y: 3, width: 58, height: 58, rx: 14, fill: "url(#dshsc-logo-bg)" }),
363
+ h("path", {
364
+ d: "M32 32 18 17M32 32 46 17M32 32 32 49",
365
+ stroke: "#ffffff",
366
+ strokeOpacity: 0.55,
367
+ strokeWidth: 2.6,
368
+ strokeLinecap: "round"
369
+ }),
370
+ h("circle", { cx: 18, cy: 17, r: 4.6, fill: "#7ef0c0" }),
371
+ h("circle", { cx: 46, cy: 17, r: 4.6, fill: "#ffffff" }),
372
+ h("circle", { cx: 32, cy: 49, r: 4.6, fill: "#ffffff" }),
373
+ h("rect", { x: 21, y: 21, width: 22, height: 22, rx: 7, fill: "#ffffff" }),
374
+ h("path", {
375
+ d: "M27 32.5l3.6 3.6 6.8-7.4",
376
+ fill: "none",
377
+ stroke: "#1d4ed8",
378
+ strokeWidth: 3,
379
+ strokeLinecap: "round",
380
+ strokeLinejoin: "round"
381
+ })
382
+ );
383
+ }
384
+ function PanelIcon({ size = 16 }) {
385
+ return h(
386
+ "svg",
387
+ {
388
+ width: size,
389
+ height: size,
390
+ viewBox: "0 0 24 24",
391
+ fill: "none",
392
+ stroke: "currentColor",
393
+ strokeWidth: 1.8,
394
+ strokeLinecap: "round",
395
+ strokeLinejoin: "round",
396
+ "aria-hidden": true,
397
+ style: { display: "block" }
398
+ },
399
+ h("rect", { x: 7.5, y: 7.5, width: 9, height: 9, rx: 3 }),
400
+ h("path", { d: "M9.8 12.1l1.5 1.5 3.1-3.3" }),
401
+ h("path", { d: "M12 7.5V4.8M12 16.5v2.7M7.5 12H4.8", opacity: 0.55 }),
402
+ h("circle", { cx: 12, cy: 4.2, r: 1.1, fill: "currentColor", stroke: "none" }),
403
+ h("circle", { cx: 12, cy: 19.8, r: 1.1, fill: "currentColor", stroke: "none" }),
404
+ h("circle", { cx: 4.2, cy: 12, r: 1.1, fill: "currentColor", stroke: "none" })
405
+ );
406
+ }
407
+ var NAV_ICON_MARKER = "data-dsh-skills-console-nav-icon";
408
+ var NAV_ROW_SELECTOR = '[role="dialog"] nav button';
409
+ function navIconMaskSvg() {
410
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><rect x="7.5" y="7.5" width="9" height="9" rx="3" stroke="#000" stroke-width="1.8"/><path d="M9.8 12.1l1.5 1.5 3.1-3.3" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 7.5V4.8M12 16.5v2.7M7.5 12H4.8" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-opacity=".55"/><circle cx="12" cy="4.2" r="1.1" fill="#000"/><circle cx="12" cy="19.8" r="1.1" fill="#000"/><circle cx="4.2" cy="12" r="1.1" fill="#000"/></svg>';
411
+ }
412
+ function navIconMaskUrl() {
413
+ return `data:image/svg+xml,${encodeURIComponent(navIconMaskSvg())}`;
414
+ }
415
+ function navIconCss(maskUrl) {
416
+ return [
417
+ `[${NAV_ICON_MARKER}] > svg { display: none; }`,
418
+ `[${NAV_ICON_MARKER}]::before {`,
419
+ " content: '';",
420
+ " flex: none;",
421
+ " width: 16px;",
422
+ " height: 16px;",
423
+ " background-color: currentColor;",
424
+ ` -webkit-mask-image: url("${maskUrl}");`,
425
+ ` mask-image: url("${maskUrl}");`,
426
+ " -webkit-mask-repeat: no-repeat;",
427
+ " mask-repeat: no-repeat;",
428
+ " -webkit-mask-position: center;",
429
+ " mask-position: center;",
430
+ " -webkit-mask-size: 16px 16px;",
431
+ " mask-size: 16px 16px;",
432
+ "}"
433
+ ].join("\n");
434
+ }
435
+ function isOwnNavRow(rowText, wantedLabel) {
436
+ const wanted = String(wantedLabel ?? "").trim();
437
+ if (wanted.length === 0) return false;
438
+ return String(rowText ?? "").trim() === wanted;
439
+ }
440
+ function installSettingsNavIcon(ctx, resolveLabel) {
441
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
442
+ ctx.effect(() => {
443
+ const tag = document.createElement("style");
444
+ tag.dataset.plugin = "@morehao/dsh-skills-console";
445
+ tag.dataset.pluginCss = "dsh-skills-console/settings-nav-icon";
446
+ tag.textContent = navIconCss(navIconMaskUrl());
447
+ document.head.appendChild(tag);
448
+ let disposed = false;
449
+ let scheduled = false;
450
+ const sync = () => {
451
+ scheduled = false;
452
+ if (disposed) return;
453
+ const wanted = resolveLabel();
454
+ for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) {
455
+ if (isOwnNavRow(row.textContent, wanted)) row.setAttribute(NAV_ICON_MARKER, "");
456
+ else row.removeAttribute(NAV_ICON_MARKER);
457
+ }
458
+ };
459
+ const schedule = () => {
460
+ if (scheduled || disposed) return;
461
+ scheduled = true;
462
+ if (typeof queueMicrotask === "function") queueMicrotask(sync);
463
+ else Promise.resolve().then(sync);
464
+ };
465
+ sync();
466
+ const observer = new MutationObserver(schedule);
467
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
468
+ return () => {
469
+ disposed = true;
470
+ observer.disconnect();
471
+ for (const row of document.querySelectorAll(`[${NAV_ICON_MARKER}]`)) row.removeAttribute(NAV_ICON_MARKER);
472
+ tag.remove();
473
+ };
474
+ }, "dsh-skills-console: settings nav icon");
475
+ }
476
+ function GitHubIcon({ size = 16 }) {
477
+ return h("svg", {
478
+ width: size,
479
+ height: size,
480
+ viewBox: "0 0 24 24",
481
+ fill: "currentColor",
482
+ "aria-hidden": true,
483
+ style: { display: "block" }
484
+ }, h("path", {
485
+ d: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.809 1.305 3.495.998.108-.776.418-1.305.762-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
486
+ }));
487
+ }
488
+ function GearIcon({ size = 13 }) {
489
+ return h(
490
+ "svg",
491
+ {
492
+ width: size,
493
+ height: size,
494
+ viewBox: "0 0 24 24",
495
+ fill: "none",
496
+ stroke: "currentColor",
497
+ strokeWidth: 1.8,
498
+ strokeLinecap: "round",
499
+ strokeLinejoin: "round",
500
+ "aria-hidden": true,
501
+ style: { display: "block" }
502
+ },
503
+ h("circle", { cx: 12, cy: 12, r: 3.1 }),
504
+ h("path", {
505
+ d: "M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.55 1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.55-1.1 1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H9a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.55 1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87V9a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"
506
+ })
507
+ );
508
+ }
509
+ function TrashIcon({ size = 14 }) {
510
+ return h(
511
+ "svg",
512
+ {
513
+ width: size,
514
+ height: size,
515
+ viewBox: "0 0 24 24",
516
+ fill: "none",
517
+ stroke: "currentColor",
518
+ strokeWidth: 1.8,
519
+ strokeLinecap: "round",
520
+ strokeLinejoin: "round",
521
+ "aria-hidden": true,
522
+ style: { display: "block" }
523
+ },
524
+ h("path", { d: "M4 7h16" }),
525
+ h("path", { d: "M9 7V5h6v2" }),
526
+ h("path", { d: "M6.5 7l1 13h9l1-13" }),
527
+ h("path", { d: "M10 11v5.5M14 11v5.5" })
528
+ );
529
+ }
530
+ function InfoIcon({ size = 13 }) {
531
+ return h(
532
+ "svg",
533
+ {
534
+ width: size,
535
+ height: size,
536
+ viewBox: "0 0 24 24",
537
+ fill: "none",
538
+ stroke: "currentColor",
539
+ strokeWidth: 1.8,
540
+ strokeLinecap: "round",
541
+ "aria-hidden": true,
542
+ style: { display: "block" }
543
+ },
544
+ h("circle", { cx: 12, cy: 12, r: 9 }),
545
+ h("path", { d: "M12 11v5" }),
546
+ h("path", { d: "M12 8h.01" })
547
+ );
548
+ }
549
+ function RefreshIcon({ size = 14 }) {
550
+ return h(
551
+ "svg",
552
+ {
553
+ width: size,
554
+ height: size,
555
+ viewBox: "0 0 24 24",
556
+ fill: "none",
557
+ stroke: "currentColor",
558
+ strokeWidth: 2,
559
+ strokeLinecap: "round",
560
+ strokeLinejoin: "round",
561
+ "aria-hidden": true,
562
+ style: { display: "block" }
563
+ },
564
+ h("path", { d: "M20 12a8 8 0 1 1-2.34-5.66" }),
565
+ h("path", { d: "M20 4v5h-5" })
566
+ );
567
+ }
568
+ function CaretIcon({ open }) {
569
+ return h("svg", {
570
+ width: 10,
571
+ height: 10,
572
+ viewBox: "0 0 24 24",
573
+ fill: "currentColor",
574
+ "aria-hidden": true,
575
+ style: { display: "block", transform: open ? "rotate(90deg)" : "none", transition: "transform .12s ease" }
576
+ }, h("path", { d: "M8 5l11 7-11 7z" }));
577
+ }
578
+ function SearchIcon({ size = 13 }) {
579
+ return h(
580
+ "svg",
581
+ {
582
+ width: size,
583
+ height: size,
584
+ viewBox: "0 0 24 24",
585
+ fill: "none",
586
+ stroke: "currentColor",
587
+ strokeWidth: 2,
588
+ strokeLinecap: "round",
589
+ "aria-hidden": true,
590
+ style: { display: "block" }
591
+ },
592
+ h("circle", { cx: 11, cy: 11, r: 6.4 }),
593
+ h("path", { d: "M16 16l4.3 4.3" })
594
+ );
595
+ }
596
+
597
+ // client/section.js
598
+ var import_react4 = __toESM(require("react"), 1);
599
+
600
+ // client/styles.js
601
+ var CSS = `
602
+ /* 主面板形态:与 DSH 自带管理页同一边距/滚动方式 */
603
+ .dshsc-page { box-sizing: border-box; width: 100%; height: 100%; overflow: auto;
604
+ padding: calc(28px + var(--dsh-frame-top-clearance, 0px)) clamp(24px, 4vw, 48px) 48px; }
605
+ .dshsc-page .dshsc-root { max-width: 980px; margin: 0 auto; }
606
+ .dshsc-page .dshsc-title { font-size: 20px; font-weight: 500; line-height: 28px; }
607
+ .dshsc-page .dshsc-sub { font-size: 13px; line-height: 20px; }
608
+ .dshsc-root { display: flex; flex-direction: column; gap: 14px; color: var(--dsw-alias-label-primary);
609
+ container-type: inline-size; container-name: dshsc; }
610
+ .dshsc-head { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
611
+ .dshsc-heading { display: flex; align-items: flex-start; gap: 10px; min-width: 0; flex: 1; }
612
+ .dshsc-heading-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
613
+ .dshsc-logo { flex: none; display: block; border-radius: 8px; }
614
+ .dshsc-title { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; }
615
+ .dshsc-sub { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin: 0; max-width: 64ch; }
616
+ .dshsc-spacer { flex: 1; }
617
+ .dshsc-head-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-left: auto; }
618
+ .dshsc-title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
619
+
620
+ /* 身份:包名 + 版本合成一枚低调 chip,不再各占一段 */
621
+ .dshsc-id { display: inline-flex; align-items: center; gap: 6px; min-width: 0; height: 22px; padding: 0 7px;
622
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); }
623
+ .dshsc-pkg { font-size: 11px; line-height: 1; color: var(--dsw-alias-label-tertiary); white-space: nowrap;
624
+ overflow: hidden; text-overflow: ellipsis; font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); }
625
+ .dshsc-idsep { flex: none; font-size: 10px; color: var(--dsw-alias-border-l3); }
626
+ .dshsc-ver { flex: none; font-size: 11px; line-height: 1; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
627
+
628
+ /* 插件级工具(设置 / GitHub):跟在身份胶囊后面,用细线分组;鬼影按钮不和业务按钮抢权重 */
629
+ .dshsc-tools { display: inline-flex; align-items: center; gap: 2px; flex: none; margin-left: 2px;
630
+ padding-left: 9px; border-left: 1px solid var(--dsw-alias-border-l1); }
631
+ .dshsc-tool { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
632
+ width: 26px; height: 26px; padding: 0; border: 0; border-radius: 7px; background: transparent;
633
+ color: var(--dsw-alias-label-secondary); cursor: pointer; text-decoration: none;
634
+ transition: background .12s ease, color .12s ease; }
635
+ .dshsc-tool:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
636
+ .dshsc-tool:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
637
+ .dshsc-tool[aria-expanded="true"] { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
638
+ .dshsc-settings { position: relative; display: inline-flex; }
639
+ /* 设置面板留在 Host 对话框内(焦点陷阱 / 层级都跟着走);开启时按左右余量选边和宽度 */
640
+ .dshsc-pop { position: absolute; top: calc(100% + 8px); z-index: 30; box-sizing: border-box;
641
+ max-width: calc(100vw - 16px); max-height: calc(100vh - 24px); overflow: auto;
642
+ display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: 12px;
643
+ border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2);
644
+ box-shadow: 0 16px 40px rgba(0, 0, 0, .35); }
645
+ .dshsc-pop-title { font-size: 11px; letter-spacing: .06em; color: var(--dsw-alias-label-tertiary); }
646
+ .dshsc-pop-label { display: flex; align-items: center; justify-content: space-between; gap: 8px;
647
+ font-size: 12px; color: var(--dsw-alias-label-primary); }
648
+ .dshsc-pop-label em { font-style: normal; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
649
+ .dshsc-pop-name { display: inline-flex; align-items: baseline; gap: 6px; }
650
+ .dshsc-pop-hint { font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
651
+
652
+ .dshsc-btn { font: inherit; font-size: 12px; line-height: 1; padding: 7px 11px; border-radius: 8px; cursor: pointer;
653
+ white-space: nowrap;
654
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary);
655
+ transition: background .12s ease, border-color .12s ease, color .12s ease; }
656
+ /* 页头动作按钮:显式同高(border-box),文案换行 / 字体回退都不会让其中一个变高 */
657
+ .dshsc-head-actions .dshsc-btn { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
658
+ height: 28px; padding: 0 11px; }
659
+ .dshsc-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
660
+ .dshsc-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
661
+ .dshsc-btn:disabled { opacity: .42; cursor: not-allowed; }
662
+ /* brand-primary 与 bg-base 在明暗主题里互为反色:文字用 bg-base 才不会是白底白字 */
663
+ .dshsc-btn.primary { background: var(--dsw-alias-brand-primary); border-color: transparent; color: var(--dsw-alias-bg-base); font-weight: 600; }
664
+ .dshsc-btn.primary:hover:not(:disabled) { background: var(--dsw-alias-brand-primary); opacity: .9; }
665
+ .dshsc-btn.tint { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border-color: transparent;
666
+ color: var(--dsw-alias-brand-primary); font-weight: 500; }
667
+ .dshsc-btn.tint:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
668
+ .dshsc-btn.ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); padding: 6px 8px; }
669
+ .dshsc-btn.ghost:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
670
+ .dshsc-btn.ghost.danger { color: var(--dsw-alias-state-error-primary); }
671
+ .dshsc-btn.ghost.danger:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); }
672
+ .dshsc-btn.ghost.dshsc-update { color: var(--dsw-alias-brand-primary); }
673
+ .dshsc-btn.ghost.dshsc-update:hover:not(:disabled) { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); }
674
+
675
+ .dshsc-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
676
+ .dshsc-tabs { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--dsw-alias-border-l1);
677
+ border-radius: 9px; background: var(--dsw-alias-bg-layer-1); }
678
+ .dshsc-tab { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; padding: 5px 10px;
679
+ border: 0; border-radius: 7px; background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
680
+ .dshsc-tab:hover { color: var(--dsw-alias-label-primary); }
681
+ .dshsc-tab.active { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary);
682
+ box-shadow: 0 0 0 1px var(--dsw-alias-border-l1), 0 1px 2px rgba(0,0,0,.12); }
683
+ .dshsc-tab .n { min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box; border-radius: 999px;
684
+ background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-tertiary);
685
+ font-size: 10px; line-height: 16px; text-align: center; font-variant-numeric: tabular-nums; }
686
+ .dshsc-tab.active .n { color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); }
687
+
688
+ .dshsc-search { position: relative; display: inline-flex; align-items: center; }
689
+ .dshsc-search > svg { position: absolute; left: 8px; pointer-events: none; color: var(--dsw-alias-label-tertiary); }
690
+ .dshsc-search input { box-sizing: border-box; width: 210px; height: 30px; padding: 0 27px 0 27px;
691
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-1);
692
+ color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; outline: none; }
693
+ .dshsc-search input::placeholder { color: var(--dsw-alias-label-tertiary); }
694
+ .dshsc-search input:focus { border-color: var(--dsw-alias-brand-primary);
695
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent); }
696
+ .dshsc-search input::-webkit-search-cancel-button { display: none; }
697
+ .dshsc-search-clear { position: absolute; right: 4px; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 6px;
698
+ background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; font-size: 13px; line-height: 1; }
699
+ .dshsc-search-clear:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
700
+
701
+ .dshsc-segment { display: inline-flex; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
702
+ background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
703
+ .dshsc-segment .dshsc-btn { display: inline-flex; align-items: center; height: 28px; border: 0; border-radius: 0;
704
+ background: transparent; padding: 0 10px; font-size: 11.5px; }
705
+ .dshsc-segment .dshsc-btn + .dshsc-btn { border-left: 1px solid var(--dsw-alias-border-l1); }
706
+ .dshsc-segment .dshsc-btn[aria-pressed="true"] { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
707
+
708
+ .dshsc-listbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
709
+ .dshsc-filters { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
710
+ .dshsc-fchip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px; border: 1px solid transparent;
711
+ border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11.5px;
712
+ cursor: pointer; white-space: nowrap; }
713
+ .dshsc-fchip:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
714
+ .dshsc-fchip.active { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); }
715
+ .dshsc-fchip:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
716
+ .dshsc-fchip .n { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
717
+ .dshsc-fchip.active .n { color: var(--dsw-alias-label-secondary); }
718
+ .dshsc-fdot { width: 5px; height: 5px; border-radius: 50%; background: var(--dsw-alias-state-warn-primary); flex: none; }
719
+ .dshsc-listbar-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; }
720
+ .dshsc-listbar .dshsc-btn { display: inline-flex; align-items: center; height: 26px; padding: 0 9px; font-size: 11.5px; }
721
+
722
+ .dshsc-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px;
723
+ background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
724
+ .dshsc-row { display: grid; grid-template-columns: 300px minmax(0, 1fr) 218px; gap: 14px; align-items: center;
725
+ padding: 8px 12px 8px 14px; min-height: 54px; border-top: 1px solid var(--dsw-alias-border-l1);
726
+ transition: background .12s ease; }
727
+ .dshsc-list > .dshsc-row:first-child { border-top: 0; }
728
+ .dshsc-row:hover { background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 48%, transparent); }
729
+ .dshsc-row.off { opacity: .7; }
730
+ .dshsc-cell { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
731
+ .dshsc-namerow { display: flex; align-items: center; gap: 7px; min-width: 0; }
732
+ .dshsc-name { font-size: 13px; font-weight: 600; letter-spacing: -.005em; min-width: 0;
733
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
734
+ .dshsc-src { display: flex; align-items: center; gap: 5px; min-width: 0; font-size: 11.5px; line-height: 16px;
735
+ color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
736
+ .dshsc-src .dshsc-code { color: var(--dsw-alias-label-secondary); font-size: 11px; }
737
+ .dshsc-note { font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary); margin-top: 2px; }
738
+ .dshsc-note.warn { color: var(--dsw-alias-state-warn-primary); }
739
+
740
+ .dshsc-tag { display: inline-flex; align-items: center; gap: 3px; flex: none; height: 18px; padding: 0 6px;
741
+ border-radius: 5px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary);
742
+ font-size: 10.5px; line-height: 1; white-space: nowrap; }
743
+ .dshsc-tag.ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 13%, transparent); color: var(--dsw-alias-state-success-primary); }
744
+ .dshsc-tag.warn { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 13%, transparent); color: var(--dsw-alias-state-warn-primary); }
745
+ .dshsc-tag.err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 13%, transparent); color: var(--dsw-alias-state-error-primary); }
746
+ .dshsc-tag.off { color: var(--dsw-alias-label-tertiary); }
747
+
748
+ .dshsc-agents { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; min-width: 0; }
749
+ .dshsc-agent { display: inline-flex; align-items: center; gap: 5px; flex: none; height: 20px; padding: 0 7px;
750
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-secondary);
751
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent); font-size: 11px; white-space: nowrap; }
752
+ .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));
753
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
754
+ .dshsc-agent .dshsc-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: .8; }
755
+ .dshsc-agent.more, .dshsc-agent.scope { border-style: dashed; background: transparent; color: var(--dsw-alias-label-tertiary); }
756
+ .dshsc-agent.more { margin: 0; font: inherit; font-size: 11px; line-height: 1; cursor: pointer;
757
+ -webkit-appearance: none; appearance: none; }
758
+ .dshsc-agent.more:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
759
+ .dshsc-agent.more[aria-expanded="true"] { color: var(--dsw-alias-label-primary);
760
+ border-color: var(--dsw-alias-border-l3); background: var(--dsw-alias-bg-layer-2); }
761
+ .dshsc-agent.more:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
762
+ .dshsc-unlinked { font-size: 11.5px; color: var(--dsw-alias-label-tertiary); }
763
+
764
+ /* “+n” 的悬浮气泡:portal 到 body + fixed 定位,列表的 overflow:hidden 裁不到 */
765
+ .dshsc-agenttip { position: fixed; z-index: 10000; box-sizing: border-box; width: max-content;
766
+ max-width: min(340px, calc(100vw - 24px)); padding: 9px 10px; display: flex; flex-direction: column; gap: 7px;
767
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-2);
768
+ box-shadow: 0 12px 32px rgba(0, 0, 0, .32), 0 2px 6px rgba(0, 0, 0, .18); pointer-events: none; }
769
+ .dshsc-agenttip-head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
770
+ font-size: 10.5px; color: var(--dsw-alias-label-tertiary); }
771
+ .dshsc-agenttip-n { min-width: 16px; height: 15px; padding: 0 5px; box-sizing: border-box; border-radius: 999px;
772
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 20%, transparent);
773
+ color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 15px; text-align: center;
774
+ font-variant-numeric: tabular-nums; }
775
+ .dshsc-agenttip-list { display: flex; flex-wrap: wrap; gap: 4px; }
776
+
777
+ .dshsc-row .dshsc-actions { display: flex; align-items: center; justify-content: flex-end; gap: 2px; flex-wrap: nowrap; }
778
+ .dshsc-row .dshsc-btn { display: inline-flex; align-items: center; justify-content: center; height: 24px;
779
+ padding: 0 7px; font-size: 11.5px; border-radius: 6px; }
780
+ /* 行内图标按钮:删除 / 检查更新 24–26px;页头工具入口见 .dshsc-tool */
781
+ .dshsc-btn.sq { box-sizing: border-box; width: 24px; padding: 0; justify-content: center; }
782
+ .dshsc-btn.sq svg { display: block; }
783
+ .dshsc-listbar .dshsc-btn.sq { width: 26px; padding: 0; }
784
+ .dshsc-sub-row .dshsc-btn.sq { width: 24px; padding: 0; }
785
+
786
+ .dshsc-groupstack { display: flex; flex-direction: column; gap: 12px; }
787
+ .dshsc-group-head { display: flex; align-items: center; gap: 8px; padding: 7px 14px; border: 1px solid var(--dsw-alias-border-l1);
788
+ border-bottom: 0; border-radius: 10px 10px 0 0; background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 48%, transparent);
789
+ font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-primary); }
790
+ .dshsc-group-head + .dshsc-list { border-radius: 0 0 10px 10px; }
791
+ .dshsc-group-head .n { min-width: 16px; height: 15px; padding: 0 5px; box-sizing: border-box; border-radius: 999px;
792
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 20%, transparent); color: var(--dsw-alias-label-secondary);
793
+ font-size: 10px; line-height: 15px; text-align: center; font-weight: 500; font-variant-numeric: tabular-nums; }
794
+ .dshsc-gdot { width: 5px; height: 5px; border-radius: 50%; background: var(--dsw-alias-brand-primary); opacity: .8; flex: none; }
795
+
796
+ /* 说明会是一整句,窄面板必然折行:图标单独成列并贴住首行,正文列负责换行,避免图标被挤成孤行 */
797
+ .dshsc-notice { display: flex; flex-direction: column; font-size: 11.5px;
798
+ color: var(--dsw-alias-label-tertiary); line-height: 1.5; }
799
+ .dshsc-notice-row { display: flex; align-items: flex-start; gap: 6px; }
800
+ .dshsc-notice-row > svg { flex: none; margin-top: 2px; }
801
+ .dshsc-notice-text { flex: 1 1 auto; min-width: 0; color: var(--dsw-alias-label-secondary); }
802
+ .dshsc-notice-text .dshsc-linkbtn { margin-left: 2px; white-space: nowrap; }
803
+ .dshsc-linkbtn { border: 0; background: none; padding: 0 2px; font: inherit; font-size: 11.5px;
804
+ color: var(--dsw-alias-brand-primary); cursor: pointer; }
805
+ .dshsc-linkbtn:hover { text-decoration: underline; }
806
+ .dshsc-linkbtn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; border-radius: 4px; }
807
+ .dshsc-notice-list { margin: 4px 0 0 19px; padding: 7px 10px; border-radius: 8px;
808
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1);
809
+ color: var(--dsw-alias-label-tertiary); font-size: 11.5px; line-height: 1.7; }
810
+ .dshsc-empty { text-align: center; padding: 30px 16px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 12px;
811
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 55%, transparent); }
812
+ .dshsc-empty .dshsc-name { font-size: 14px; }
813
+ .dshsc-empty .dshsc-meta { justify-content: center; margin-top: 6px; }
814
+ .dshsc-meta { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; }
815
+
816
+ .dshsc-banner { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 9px 12px; font-size: 12px;
817
+ line-height: 1.55; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary);
818
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
819
+ .dshsc-banner.err { color: var(--dsw-alias-state-error-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, var(--dsw-alias-border-l1)); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
820
+ .dshsc-banner.warn { color: var(--dsw-alias-state-warn-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, var(--dsw-alias-border-l1)); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent); }
821
+
822
+ .dshsc-sub-row { display: flex; align-items: center; gap: 8px; padding: 8px 12px; min-height: 52px; border-top: 1px solid var(--dsw-alias-border-l1); }
823
+ .dshsc-sub-block:first-child .dshsc-sub-row { border-top: 0; }
824
+ .dshsc-sub-block + .dshsc-sub-block { border-top: 1px solid var(--dsw-alias-border-l1); }
825
+ .dshsc-caret { flex: none; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 6px; background: transparent;
826
+ color: var(--dsw-alias-label-secondary); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; }
827
+ .dshsc-caret:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
828
+ .dshsc-caret:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
829
+ .dshsc-sub-row .dshsc-name { font-size: 12.5px; }
830
+ .dshsc-substat { font-size: 11px; color: var(--dsw-alias-label-tertiary); white-space: nowrap; }
831
+ .dshsc-sub-row .dshsc-actions { margin-left: auto; display: flex; align-items: center; gap: 2px; }
832
+ .dshsc-sub-row .dshsc-btn { height: 24px; padding: 0 7px; font-size: 11.5px; display: inline-flex; align-items: center; }
833
+ .dshsc-sub-list { display: flex; flex-direction: column; background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 22%, transparent); }
834
+ .dshsc-sub-entry { display: flex; align-items: center; gap: 8px;
835
+ font-size: 12px; padding: 6px 12px 6px 44px; border-top: 1px solid var(--dsw-alias-border-l1); }
836
+ .dshsc-sub-entry:first-child { border-top: 0; }
837
+ .dshsc-sub-entry .dshsc-name { font-size: 12px; font-weight: 500; }
838
+ .dshsc-state { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
839
+ .dshsc-state.ok svg { color: var(--dsw-alias-state-success-primary); }
840
+ .dshsc-sub-entry .dshsc-actions { margin-left: auto; display: flex; justify-content: flex-end; }
841
+ .dshsc-sub-entry .dshsc-btn { height: 24px; padding: 0 9px; font-size: 11.5px; }
842
+ /* 设置页的真实宽度常常远小于窗口(侧栏 + 页面缩放),视口 media query 不可靠;
843
+ 以 .dshsc-root 自身宽度为准,窄面板时切两列,避免固定列把 Agent 标签挤成竖排。 */
844
+ @container dshsc (max-width: 900px) {
845
+ .dshsc-head-actions { width: 100%; justify-content: flex-end; }
846
+ .dshsc-search input { width: 180px; }
847
+ .dshsc-row { grid-template-columns: minmax(0, 1fr) 196px; row-gap: 3px; padding: 9px 12px; }
848
+ .dshsc-cell { grid-column: 1; }
849
+ .dshsc-agents { grid-column: 1; }
850
+ .dshsc-row .dshsc-actions { grid-column: 2; grid-row: 1 / span 2; }
851
+ .dshsc-row .dshsc-btn { padding: 0 5px; }
852
+ }
853
+ /* 窄面板里工具组会换到下一行,行首的分割线就没有意义了 */
854
+ @container dshsc (max-width: 520px) {
855
+ .dshsc-tools { border-left: 0; padding-left: 0; margin-left: 0; }
856
+ }
857
+ .dshsc-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
858
+
859
+ /* 与 DSH 自带开关同尺寸/同 token:关是 border-l3 轨道,开是 brand-primary 轨道 */
860
+ .dshsc-switch { box-sizing: border-box; position: relative; flex: none; width: 36px; height: 20px; padding: 2px;
861
+ border: 0; border-radius: 999px; background: var(--dsw-alias-border-l3); cursor: pointer;
862
+ transition: background .14s ease; }
863
+ .dshsc-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
864
+ .dshsc-switch:disabled { cursor: not-allowed; opacity: .5; }
865
+ .dshsc-switch[aria-checked="true"] { background: var(--dsw-alias-brand-primary); }
866
+ .dshsc-knob { display: block; width: 16px; height: 16px; border-radius: 50%;
867
+ background: var(--dsw-alias-switch-thumb); box-shadow: 0 1px 2px rgba(0, 0, 0, .28);
868
+ transition: transform .12s ease, background .12s ease; }
869
+ .dshsc-switch[aria-checked="true"] .dshsc-knob { transform: translateX(16px); background: var(--dsw-alias-label-primary-foreground); }
870
+
871
+ .dshsc-overlay { position: fixed; inset: 0; background: rgba(4,6,9,.55); display: flex; align-items: center; justify-content: center;
872
+ z-index: 60; padding: 20px; backdrop-filter: blur(2px); }
873
+ .dshsc-modal { position: relative; width: min(600px, 100%); max-height: calc(100vh - 40px); overflow: auto;
874
+ background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 14px;
875
+ padding: 18px; display: flex; flex-direction: column; gap: 14px;
876
+ box-shadow: 0 20px 56px rgba(0,0,0,.38), 0 2px 8px rgba(0,0,0,.18); }
877
+ /* 64ch 是主页面长文的舒适行长;弹窗说明只有一两句,用满弹窗宽度,避免没必要的折行 */
878
+ .dshsc-modal .dshsc-sub { max-width: none; }
879
+ .dshsc-modal h3 { margin: 0; font-size: 15px; font-weight: 600; padding-right: 26px; }
880
+ .dshsc-modal-close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border: 0; border-radius: 7px;
881
+ background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; cursor: pointer; }
882
+ .dshsc-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
883
+
884
+ .dshsc-group { margin: 2px 0 -2px; font-size: 11px; font-weight: 600; letter-spacing: .02em;
885
+ color: var(--dsw-alias-label-secondary); }
886
+ .dshsc-field { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
887
+ .dshsc-field input { font: inherit; font-size: 13px; padding: 8px 10px; border-radius: 8px;
888
+ border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); outline: none; }
889
+ .dshsc-field input:focus { border-color: var(--dsw-alias-brand-primary);
890
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
891
+
892
+ .dshsc-check { display: flex; align-items: center; gap: 9px; font-size: 13px; padding: 5px 8px; border-radius: 8px; cursor: pointer; }
893
+ .dshsc-check:hover { background: var(--dsw-alias-bg-layer-1); }
894
+ .dshsc-check input[type="checkbox"], .dshsc-check input[type="radio"] { appearance: none; -webkit-appearance: none;
895
+ flex: none; position: relative; width: 15px; height: 15px; margin: 0; border: 1px solid var(--dsw-alias-border-l2);
896
+ border-radius: 4px; background: var(--dsw-alias-bg-layer-1); cursor: pointer; }
897
+ .dshsc-check input[type="radio"] { border-radius: 50%; }
898
+ .dshsc-check input[type="checkbox"]:checked { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); }
899
+ /* 选中用反色圆点:品牌色可能是白色,白勾会消失 */
900
+ .dshsc-check input[type="checkbox"]:checked::after { content: ''; position: absolute; left: 50%; top: 50%;
901
+ width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-bg-base);
902
+ transform: translate(-50%, -50%); }
903
+ .dshsc-check input[type="radio"]:checked { border: 5px solid var(--dsw-alias-brand-primary); }
904
+ .dshsc-check input:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
905
+ .dshsc-check input:disabled { opacity: .55; cursor: not-allowed; }
906
+ .dshsc-check small { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
907
+ .dshsc-check span { white-space: nowrap; }
908
+
909
+ .dshsc-panel { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1);
910
+ padding: 4px; max-height: 300px; overflow: auto; display: flex; flex-direction: column; }
911
+ .dshsc-panel .dshsc-check:hover { background: var(--dsw-alias-bg-layer-2); }
912
+
913
+ .dshsc-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
914
+ .dshsc-toast { position: fixed; right: 18px; bottom: 18px; z-index: 70; font-size: 12px; padding: 10px 13px; border-radius: 9px;
915
+ background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2);
916
+ box-shadow: 0 8px 24px rgba(0,0,0,.22); max-width: 420px; }
917
+ .dshsc-toast.err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
918
+ .dshsc-toast.ok { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 50%, var(--dsw-alias-border-l2)); }
919
+ `;
920
+
921
+ // client/actions.js
922
+ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal }) {
923
+ async function adopt(row) {
924
+ await run(`adopt:${row.name}`, async () => {
925
+ const result = await apiCall("POST", `/skills/${encodeURIComponent(row.name)}/adopt`, {});
926
+ setToast({ kind: "ok", text: `${row.name} · ${result.tracked ? t("adopt.done") : t("adopt.noUpstream")}` });
927
+ return result;
928
+ });
929
+ }
930
+ async function toggleMode(row) {
931
+ const mode = row.mode === "on" ? "off" : "on";
932
+ await run(`mode:${row.name}`, () => apiCall("POST", `/skills/${encodeURIComponent(row.name)}/mode`, { mode }));
933
+ }
934
+ async function updateOne(row, overwrite) {
935
+ await run(`update:${row.name}`, async () => {
936
+ const result = await apiCall("POST", "/updates/apply", { name: row.name, overwrite });
937
+ setToast({ kind: "ok", text: `${row.name} → ${short(result.toCommit) || "latest"}` });
938
+ return result;
939
+ });
940
+ }
941
+ function requestUpdate(row) {
942
+ if (row.status === "locally-modified" || row.status === "conflict") {
943
+ setModal({
944
+ type: "confirm-update",
945
+ row,
946
+ onConfirm: () => {
947
+ setModal(null);
948
+ void updateOne(row, true);
949
+ }
332
950
  });
333
- let json = {};
951
+ return;
952
+ }
953
+ void updateOne(row, false);
954
+ }
955
+ async function updateAll() {
956
+ const pending = rows.filter((row) => row.status === "update-available");
957
+ for (const row of pending) {
334
958
  try {
335
- json = await response.json();
959
+ await run(`update:${row.name}`, () => apiCall("POST", "/updates/apply", { name: row.name }));
336
960
  } catch {
337
- json = {};
338
961
  }
339
- if (!response.ok || json.ok === false) {
340
- const error = new Error(json.message || `HTTP ${response.status}`);
341
- error.code = json.code;
342
- error.detail = json.detail;
343
- throw error;
344
- }
345
- return json;
346
962
  }
347
-
348
- function buildTranslate(locale) {
349
- const active = () => {
963
+ setToast({ kind: "ok", text: t("action.updateAll") });
964
+ }
965
+ async function checkUpdates() {
966
+ await run("check", async () => {
967
+ const result = await apiCall("POST", "/updates/check", {});
968
+ setToast({ kind: "ok", text: `${t("toast.checked")} · ${result.checks.length}` });
969
+ });
970
+ }
971
+ function requestDelete(row) {
972
+ setModal({
973
+ type: "confirm-delete",
974
+ row,
975
+ onConfirm: async () => {
976
+ setModal(null);
977
+ setBusy(`delete:${row.name}`);
350
978
  try {
351
- return locale?.getLocale?.().active || 'zh';
352
- } catch {
353
- return 'zh';
979
+ await apiCall("DELETE", `/skills/${encodeURIComponent(row.name)}`);
980
+ setToast({ kind: "ok", text: `${row.name} · ${t("action.delete")} ✓` });
981
+ await reload();
982
+ } catch (err) {
983
+ if (err.code === "error.skills.remove-conflict") {
984
+ setModal({ type: "conflict", row, detail: err.detail, message: err.message });
985
+ } else {
986
+ setToast({ kind: "err", text: err.message });
987
+ }
988
+ } finally {
989
+ setBusy(null);
354
990
  }
355
- };
356
- return (key) => {
357
- const id = active();
358
- const pack = DICT[id] || DICT.zh;
359
- return pack[key] ?? DICT.zh[key] ?? key;
360
- };
991
+ }
992
+ });
993
+ }
994
+ async function saveLinks(row, targets, scopeProjects) {
995
+ const current = new Set(row.links.map((link) => link.target));
996
+ const desired = new Set(scopeProjects.length > 0 ? targets.filter((target) => target !== "dsh-global") : targets);
997
+ const toRemove = [...current].filter((target) => !desired.has(target));
998
+ await run(`links:${row.name}`, async () => {
999
+ if (toRemove.length > 0) {
1000
+ await apiCall("POST", "/links", { skill: row.name, targets: toRemove, action: "remove" });
1001
+ }
1002
+ await apiCall("POST", "/links", { skill: row.name, targets: [...desired], action: "add", scopeProjects });
1003
+ });
1004
+ setToast({ kind: "ok", text: `${row.name} · ${t("action.links")} ✓` });
1005
+ }
1006
+ function requestAddSkill() {
1007
+ setModal({ type: "add-skill" });
1008
+ }
1009
+ function requestAddSubscription() {
1010
+ setModal({ type: "add-subscription" });
1011
+ }
1012
+ async function addSkill(form) {
1013
+ const names = form.skills.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
1014
+ setModal(null);
1015
+ await run("install", async () => {
1016
+ const result = await apiCall("POST", "/install", { source: form.source.trim(), skills: names });
1017
+ const warnings = result.warnings?.length || 0;
1018
+ setToast({
1019
+ kind: warnings ? "err" : "ok",
1020
+ text: `${result.installed?.length || 0} ${t("skillCount")}${warnings ? ` · ${warnings} 条提示` : ""}`
1021
+ });
1022
+ });
1023
+ }
1024
+ async function addSubscription(form) {
1025
+ setModal(null);
1026
+ await run("subscribe", async () => {
1027
+ const result = await apiCall("POST", "/subscriptions", {
1028
+ source: form.source.trim(),
1029
+ ref: form.ref.trim() || void 0,
1030
+ subpath: form.subpath.trim() || void 0
1031
+ });
1032
+ setToast({ kind: "ok", text: result.subscription?.source || form.source });
1033
+ });
1034
+ }
1035
+ async function refreshSubscription(row) {
1036
+ await run(`refresh:${row.id}`, async () => {
1037
+ const result = await apiCall("POST", `/subscriptions/${encodeURIComponent(row.id)}/refresh`, {});
1038
+ setToast({ kind: "ok", text: `${row.source} · ${result.changed ? "snapshot updated" : "unchanged"}` });
1039
+ });
1040
+ }
1041
+ async function deleteSubscription(row) {
1042
+ await run(`unsubscribe:${row.id}`, async () => {
1043
+ await apiCall("DELETE", `/subscriptions/${encodeURIComponent(row.id)}`, { keepInstalled: true });
1044
+ setToast({ kind: "ok", text: `${row.source} · ${t("action.delete")} ✓` });
1045
+ });
1046
+ }
1047
+ async function installFromSubscription(subscription, entry) {
1048
+ await run(`install:${entry.name}`, async () => {
1049
+ await apiCall("POST", "/install", { subscriptionId: subscription.id, skills: [entry.name] });
1050
+ setToast({ kind: "ok", text: `${entry.name} ✓` });
1051
+ });
1052
+ }
1053
+ return {
1054
+ adopt,
1055
+ toggleMode,
1056
+ requestUpdate,
1057
+ updateAll,
1058
+ checkUpdates,
1059
+ requestDelete,
1060
+ saveLinks,
1061
+ requestAddSkill,
1062
+ requestAddSubscription,
1063
+ addSkill,
1064
+ addSubscription,
1065
+ refreshSubscription,
1066
+ deleteSubscription,
1067
+ installFromSubscription
1068
+ };
1069
+ }
1070
+
1071
+ // client/hooks.js
1072
+ var import_react2 = __toESM(require("react"), 1);
1073
+ var TOAST_MS = 4200;
1074
+ function useSkillsData() {
1075
+ const [data, setData] = import_react2.default.useState(null);
1076
+ const [error, setError] = import_react2.default.useState(null);
1077
+ const [busy, setBusy] = import_react2.default.useState(null);
1078
+ const [toast, setToast] = import_react2.default.useState(null);
1079
+ const reload = import_react2.default.useCallback(async () => {
1080
+ try {
1081
+ const next = await apiCall("GET", "/state");
1082
+ setData(next);
1083
+ setError(null);
1084
+ } catch (err) {
1085
+ setError(err.message);
361
1086
  }
362
-
363
- const GITHUB_URL = 'https://github.com/morehao/dsh-skills-console';
364
-
365
- function Logo({ size = 30 }) {
366
- return h('svg', {
367
- className: 'dshsc-logo', width: size, height: size, viewBox: '0 0 64 64', 'aria-hidden': true,
368
- },
369
- h('defs', null,
370
- h('linearGradient', { id: 'dshsc-logo-bg', x1: 6, y1: 2, x2: 58, y2: 62, gradientUnits: 'userSpaceOnUse' },
371
- h('stop', { stopColor: '#3b82f6' }),
372
- h('stop', { offset: 1, stopColor: '#1d4ed8' }))),
373
- h('rect', { x: 3, y: 3, width: 58, height: 58, rx: 14, fill: 'url(#dshsc-logo-bg)' }),
374
- h('path', {
375
- d: 'M32 32 18 17M32 32 46 17M32 32 32 49',
376
- stroke: '#ffffff', strokeOpacity: 0.55, strokeWidth: 2.6, strokeLinecap: 'round',
1087
+ }, []);
1088
+ import_react2.default.useEffect(() => {
1089
+ void reload();
1090
+ }, [reload]);
1091
+ import_react2.default.useEffect(() => {
1092
+ if (!toast) return void 0;
1093
+ const timer = setTimeout(() => setToast(null), TOAST_MS);
1094
+ return () => clearTimeout(timer);
1095
+ }, [toast]);
1096
+ const run = import_react2.default.useCallback(
1097
+ async (label, task) => {
1098
+ setBusy(label);
1099
+ try {
1100
+ const result = await task();
1101
+ await reload();
1102
+ return result;
1103
+ } catch (err) {
1104
+ setToast({ kind: "err", text: err.message });
1105
+ throw err;
1106
+ } finally {
1107
+ setBusy(null);
1108
+ }
1109
+ },
1110
+ [reload]
1111
+ );
1112
+ return { data, error, busy, toast, setToast, setBusy, reload, run };
1113
+ }
1114
+ function useSettingsPopover() {
1115
+ const [settingsOpen, setSettingsOpen] = import_react2.default.useState(false);
1116
+ const settingsRef = import_react2.default.useRef(null);
1117
+ const [settingsPlacement, setSettingsPlacement] = import_react2.default.useState({ side: "left", width: 286 });
1118
+ import_react2.default.useEffect(() => {
1119
+ if (!settingsOpen || typeof document === "undefined") return void 0;
1120
+ const close = () => setSettingsOpen(false);
1121
+ const onPointerDown = (event) => {
1122
+ const target = event?.target;
1123
+ if (target && typeof target.closest === "function" && (target.closest(".dshsc-settings") || target.closest(".dshsc-pop"))) return;
1124
+ close();
1125
+ };
1126
+ const onKeyDown = (event) => {
1127
+ if (event?.key === "Escape") close();
1128
+ };
1129
+ document.addEventListener("pointerdown", onPointerDown);
1130
+ document.addEventListener("keydown", onKeyDown);
1131
+ window.addEventListener("resize", close);
1132
+ return () => {
1133
+ document.removeEventListener("pointerdown", onPointerDown);
1134
+ document.removeEventListener("keydown", onKeyDown);
1135
+ window.removeEventListener("resize", close);
1136
+ };
1137
+ }, [settingsOpen]);
1138
+ function toggleSettings() {
1139
+ if (settingsOpen) {
1140
+ setSettingsOpen(false);
1141
+ return;
1142
+ }
1143
+ const anchor = settingsRef.current;
1144
+ if (anchor && typeof window !== "undefined") {
1145
+ const rect = anchor.getBoundingClientRect();
1146
+ const root = typeof anchor.closest === "function" ? anchor.closest(".dshsc-root") : null;
1147
+ const rootRect = root ? root.getBoundingClientRect() : { left: 8, right: window.innerWidth - 8 };
1148
+ const spaceRight = rootRect.right - rect.left;
1149
+ const spaceLeft = rect.right - rootRect.left;
1150
+ const side = spaceRight >= spaceLeft ? "right" : "left";
1151
+ const width = Math.max(180, Math.min(286, (side === "right" ? spaceRight : spaceLeft) - 8));
1152
+ setSettingsPlacement({ side, width });
1153
+ }
1154
+ setSettingsOpen(true);
1155
+ }
1156
+ return { settingsOpen, settingsRef, settingsPlacement, closeSettings: () => setSettingsOpen(false), toggleSettings };
1157
+ }
1158
+ function useAgentTip() {
1159
+ const [agentTip, setAgentTip] = import_react2.default.useState(null);
1160
+ const [agentTipPos, setAgentTipPos] = import_react2.default.useState(null);
1161
+ const agentTipRef = import_react2.default.useRef(null);
1162
+ import_react2.default.useEffect(() => {
1163
+ if (!agentTip) return void 0;
1164
+ const close = () => {
1165
+ setAgentTip(null);
1166
+ setAgentTipPos(null);
1167
+ };
1168
+ const onPointerDown = (event) => {
1169
+ if (agentTip.anchor && event?.target === agentTip.anchor) return;
1170
+ close();
1171
+ };
1172
+ const onKeyDown = (event) => {
1173
+ if (event?.key === "Escape") close();
1174
+ };
1175
+ window.addEventListener("scroll", close, true);
1176
+ window.addEventListener("resize", close);
1177
+ if (typeof document !== "undefined") {
1178
+ document.addEventListener("pointerdown", onPointerDown, true);
1179
+ document.addEventListener("keydown", onKeyDown);
1180
+ }
1181
+ return () => {
1182
+ window.removeEventListener("scroll", close, true);
1183
+ window.removeEventListener("resize", close);
1184
+ if (typeof document !== "undefined") {
1185
+ document.removeEventListener("pointerdown", onPointerDown, true);
1186
+ document.removeEventListener("keydown", onKeyDown);
1187
+ }
1188
+ };
1189
+ }, [agentTip]);
1190
+ import_react2.default.useLayoutEffect(() => {
1191
+ if (!agentTip || typeof window === "undefined") return;
1192
+ const node = agentTipRef.current;
1193
+ if (!node) return;
1194
+ const width = node.offsetWidth || 0;
1195
+ const height = node.offsetHeight || 0;
1196
+ const { left, right, top, bottom } = agentTip.rect;
1197
+ const nextLeft = Math.max(8, Math.min(left + (right - left) / 2 - width / 2, window.innerWidth - width - 8));
1198
+ let nextTop = bottom + 6;
1199
+ if (nextTop + height > window.innerHeight - 8) nextTop = Math.max(8, top - height - 6);
1200
+ setAgentTipPos({ left: nextLeft, top: nextTop });
1201
+ }, [agentTip]);
1202
+ function openAgentTip(event, rowName, labels) {
1203
+ if (agentTip?.rowName === rowName) return;
1204
+ const anchor = event?.currentTarget;
1205
+ if (!anchor || typeof anchor.getBoundingClientRect !== "function") return;
1206
+ const rect = anchor.getBoundingClientRect();
1207
+ setAgentTipPos(null);
1208
+ setAgentTip({
1209
+ anchor,
1210
+ rowName,
1211
+ labels,
1212
+ rect: { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom }
1213
+ });
1214
+ }
1215
+ function closeAgentTip() {
1216
+ setAgentTip(null);
1217
+ setAgentTipPos(null);
1218
+ }
1219
+ return { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip };
1220
+ }
1221
+
1222
+ // client/views/header.js
1223
+ function Header({
1224
+ t,
1225
+ page,
1226
+ plugin,
1227
+ writable,
1228
+ autoLinkLabel,
1229
+ autoLinkCount,
1230
+ tab,
1231
+ settingsOpen,
1232
+ settingsRef,
1233
+ settingsPlacement,
1234
+ placement,
1235
+ placementBusy,
1236
+ readOnly,
1237
+ onToggleSettings,
1238
+ onChangePlacement,
1239
+ onOpenAutoLink,
1240
+ onAddSkill,
1241
+ onAddSubscription
1242
+ }) {
1243
+ const headTools = h(
1244
+ "div",
1245
+ { className: "dshsc-tools" },
1246
+ // 先源码、后设置:GitHub 紧挨包名,齿轮留在工具组最外侧
1247
+ h("a", {
1248
+ className: "dshsc-tool",
1249
+ href: GITHUB_URL,
1250
+ target: "_blank",
1251
+ rel: "noopener noreferrer",
1252
+ title: t("github.open"),
1253
+ "aria-label": t("github.open")
1254
+ }, h(GitHubIcon, { size: 15 })),
1255
+ h(
1256
+ "div",
1257
+ { className: "dshsc-settings" },
1258
+ h("button", {
1259
+ ref: settingsRef,
1260
+ className: "dshsc-tool",
1261
+ "aria-haspopup": "dialog",
1262
+ "aria-expanded": String(settingsOpen),
1263
+ type: "button",
1264
+ "aria-label": t("settings.title"),
1265
+ title: t("settings.title"),
1266
+ onClick: onToggleSettings
1267
+ }, h(GearIcon, { size: 15 })),
1268
+ settingsOpen ? h(
1269
+ "div",
1270
+ {
1271
+ className: "dshsc-pop",
1272
+ role: "dialog",
1273
+ "aria-label": t("settings.title"),
1274
+ style: settingsPlacement.side === "right" ? { left: 0, width: `${settingsPlacement.width}px` } : { right: 0, width: `${settingsPlacement.width}px` }
1275
+ },
1276
+ h("div", { className: "dshsc-pop-title" }, t("settings.title")),
1277
+ h(
1278
+ "div",
1279
+ { className: "dshsc-pop-label" },
1280
+ h(
1281
+ "span",
1282
+ { className: "dshsc-pop-name" },
1283
+ t("placement.label"),
1284
+ h("em", null, t("placement.effect"))
1285
+ ),
1286
+ h("button", {
1287
+ className: "dshsc-switch",
1288
+ role: "switch",
1289
+ "aria-checked": String(placement === "sidebar"),
1290
+ "aria-label": `${t("placement.label")}:${placement === "sidebar" ? t("placement.on") : t("placement.off")}`,
1291
+ title: t("placement.hint"),
1292
+ disabled: Boolean(placementBusy) || readOnly,
1293
+ onClick: () => void onChangePlacement(placement === "sidebar" ? "settings" : "sidebar")
1294
+ }, h("span", { className: "dshsc-knob" }))
1295
+ ),
1296
+ h("div", { className: "dshsc-pop-hint" }, t("placement.hint"))
1297
+ ) : null
1298
+ )
1299
+ );
1300
+ return h(
1301
+ "div",
1302
+ { className: "dshsc-head" },
1303
+ h(
1304
+ "div",
1305
+ { className: "dshsc-heading" },
1306
+ h(Logo, { size: page ? 36 : 30 }),
1307
+ h(
1308
+ "div",
1309
+ { className: "dshsc-heading-text" },
1310
+ h(
1311
+ "div",
1312
+ { className: "dshsc-title-row" },
1313
+ h("h2", { className: "dshsc-title" }, page ? t("page.title") : t("section.title")),
1314
+ // 身份:包名 + 版本合成一枚低调 chip,由 Host 从 package.json 注入
1315
+ plugin?.name ? h(
1316
+ "span",
1317
+ { className: "dshsc-id" },
1318
+ h("span", { className: "dshsc-pkg" }, plugin.name),
1319
+ plugin.version ? h("span", { className: "dshsc-idsep" }, "|") : null,
1320
+ plugin.version ? h(
1321
+ "span",
1322
+ { className: "dshsc-ver", title: `${t("version.label")} v${plugin.version}` },
1323
+ `v${plugin.version}`
1324
+ ) : null
1325
+ ) : null,
1326
+ headTools
1327
+ ),
1328
+ h("p", { className: "dshsc-sub" }, t("section.subtitle"))
1329
+ )
1330
+ ),
1331
+ // 右侧只留业务动作;主按钮仍在最右
1332
+ h(
1333
+ "div",
1334
+ { className: "dshsc-head-actions" },
1335
+ h("button", {
1336
+ className: "dshsc-btn",
1337
+ disabled: !writable,
1338
+ title: `${t("autoLink")}:${autoLinkLabel}`,
1339
+ "aria-label": `${t("autoLink")}:${autoLinkLabel}`,
1340
+ onClick: onOpenAutoLink
1341
+ }, `${t("autoLink")} · ${autoLinkCount} ▾`),
1342
+ h("button", {
1343
+ className: "dshsc-btn primary",
1344
+ disabled: !writable,
1345
+ onClick: tab === "installed" ? onAddSkill : onAddSubscription
1346
+ }, tab === "installed" ? t("action.addSkill") : t("action.addSubscription"))
1347
+ )
1348
+ );
1349
+ }
1350
+
1351
+ // client/views/toolbar.js
1352
+ function Toolbar({
1353
+ t,
1354
+ tab,
1355
+ onTabChange,
1356
+ installedCount,
1357
+ subscriptionCount,
1358
+ query,
1359
+ onQueryChange,
1360
+ view,
1361
+ onViewChange
1362
+ }) {
1363
+ return h(
1364
+ "div",
1365
+ { className: "dshsc-toolbar" },
1366
+ h(
1367
+ "div",
1368
+ { className: "dshsc-tabs" },
1369
+ h(
1370
+ "button",
1371
+ { className: `dshsc-tab${tab === "installed" ? " active" : ""}`, onClick: () => onTabChange("installed") },
1372
+ h("span", null, t("tab.installed")),
1373
+ h("span", { className: "n" }, installedCount)
1374
+ ),
1375
+ h(
1376
+ "button",
1377
+ { className: `dshsc-tab${tab === "subscriptions" ? " active" : ""}`, onClick: () => onTabChange("subscriptions") },
1378
+ h("span", null, t("tab.subscriptions")),
1379
+ h("span", { className: "n" }, subscriptionCount)
1380
+ )
1381
+ ),
1382
+ h("div", { className: "dshsc-spacer" }),
1383
+ tab === "installed" ? [
1384
+ h(
1385
+ "div",
1386
+ { key: "search", className: "dshsc-search" },
1387
+ h(SearchIcon, { size: 13 }),
1388
+ h("input", {
1389
+ type: "search",
1390
+ value: query,
1391
+ placeholder: t("search.placeholder"),
1392
+ "aria-label": t("search.placeholder"),
1393
+ onChange: (event) => onQueryChange(event.target.value)
1394
+ }),
1395
+ query ? h("button", {
1396
+ className: "dshsc-search-clear",
1397
+ "aria-label": t("search.clear"),
1398
+ title: t("search.clear"),
1399
+ onClick: () => onQueryChange("")
1400
+ }, "×") : null
1401
+ ),
1402
+ h(
1403
+ "div",
1404
+ { key: "view", className: "dshsc-segment" },
1405
+ h("button", { className: "dshsc-btn", "aria-pressed": String(view === "flat"), onClick: () => onViewChange("flat") }, t("view.flat")),
1406
+ h("button", { className: "dshsc-btn", "aria-pressed": String(view === "agent"), onClick: () => onViewChange("agent") }, t("view.agent"))
1407
+ )
1408
+ ] : null
1409
+ );
1410
+ }
1411
+
1412
+ // client/views/installed.js
1413
+ var UPDATABLE = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
1414
+ function InstalledTab({
1415
+ t,
1416
+ data,
1417
+ rows,
1418
+ visibleRows,
1419
+ counts,
1420
+ bulkCount,
1421
+ filter,
1422
+ query,
1423
+ view,
1424
+ busy,
1425
+ writable,
1426
+ agentTip,
1427
+ builtinOpen,
1428
+ actions
1429
+ }) {
1430
+ const updatable = (row) => UPDATABLE.has(row.status);
1431
+ function statusTag(row) {
1432
+ if (row.status === "unknown") return null;
1433
+ const map = {
1434
+ current: ["status.current", "ok", true],
1435
+ "update-available": ["status.update", "warn"],
1436
+ "locally-modified": ["status.local", "warn"],
1437
+ conflict: ["status.conflict", "warn"],
1438
+ "upstream-deleted": ["status.deleted", "warn"],
1439
+ "broken-link": ["status.broken", "err"],
1440
+ unmanaged: ["status.unmanaged", ""]
1441
+ };
1442
+ const [key, cls, check] = map[row.status] || ["status.unknown", ""];
1443
+ return h("span", { className: `dshsc-tag ${cls}` }, check ? `✓ ${t(key)}` : t(key));
1444
+ }
1445
+ function sourceLine(row) {
1446
+ if (!row.source) {
1447
+ return h("div", { className: "dshsc-src", title: t("meta.noUpstream") }, t("meta.noUpstream"));
1448
+ }
1449
+ const ref = row.commit ? `#${short(row.commit)}` : row.ref ? `#${row.ref}` : "";
1450
+ const text = `${row.source}${ref}`;
1451
+ return h(
1452
+ "div",
1453
+ { className: "dshsc-src", title: text },
1454
+ h("span", null, `${t("meta.from")} `),
1455
+ h("span", { className: "dshsc-code" }, text)
1456
+ );
1457
+ }
1458
+ function agentChips(row) {
1459
+ const links = row.links.filter((link) => !link.broken);
1460
+ const projectLinks = links.filter((link) => String(link.target).startsWith("project:"));
1461
+ const labels = links.filter((link) => !String(link.target).startsWith("project:")).map((link) => link.target === "dsh-global" ? "DSH" : link.label);
1462
+ if (projectLinks.length > 0 && !labels.includes("DSH")) labels.unshift("DSH");
1463
+ const projects = row.scopeProjects || [];
1464
+ const scopeNames = projects.length > 0 ? projects.map((project) => project.split("/").pop()) : projectLinks.map((link) => link.label);
1465
+ if (labels.length === 0 && scopeNames.length === 0) {
1466
+ return h("span", { className: "dshsc-unlinked" }, t("meta.unlinked"));
1467
+ }
1468
+ const chips = labels.slice(0, 3).map((label, index) => h("span", {
1469
+ className: `dshsc-agent${label === "DSH" ? " dsh" : ""}`,
1470
+ key: `agent-${index}`,
1471
+ title: label
1472
+ }, label === "DSH" ? h("i", { className: "dshsc-dot" }) : null, label));
1473
+ if (labels.length > 3) {
1474
+ const open = agentTip?.rowName === row.name;
1475
+ chips.push(h("button", {
1476
+ type: "button",
1477
+ className: "dshsc-agent more",
1478
+ key: "more",
1479
+ "aria-label": `${t("meta.agentsAll")}:${labels.join("、")}`,
1480
+ "aria-describedby": open ? "dshsc-agenttip" : void 0,
1481
+ "aria-expanded": String(open),
1482
+ onClick: (event) => actions.onOpenAgentTip(event, row.name, labels),
1483
+ onMouseEnter: (event) => actions.onOpenAgentTip(event, row.name, labels),
1484
+ onFocus: (event) => actions.onOpenAgentTip(event, row.name, labels),
1485
+ onMouseLeave: actions.onCloseAgentTip,
1486
+ onBlur: actions.onCloseAgentTip
1487
+ }, `+${labels.length - 3}`));
1488
+ }
1489
+ if (scopeNames.length > 0) {
1490
+ chips.push(h("span", {
1491
+ className: "dshsc-agent scope",
1492
+ key: "scope",
1493
+ title: `${t("meta.scope")}:${projects.length > 0 ? projects.join("、") : scopeNames.join("、")}`
1494
+ }, `${t("meta.onlyProject")} ${scopeNames.join("、")}`));
1495
+ }
1496
+ return chips;
1497
+ }
1498
+ function rowView(row) {
1499
+ const off = row.mode === "off";
1500
+ const tracked = row.status !== "unmanaged";
1501
+ const canUpdate = writable && updatable(row);
1502
+ return h(
1503
+ "div",
1504
+ { className: `dshsc-row${off ? " off" : ""}`, key: row.name },
1505
+ h(
1506
+ "div",
1507
+ { className: "dshsc-cell" },
1508
+ h(
1509
+ "div",
1510
+ { className: "dshsc-namerow" },
1511
+ h("span", { className: "dshsc-name", title: row.name }, row.name),
1512
+ statusTag(row),
1513
+ off ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null
1514
+ ),
1515
+ sourceLine(row),
1516
+ off ? h("div", { className: "dshsc-note" }, t("mode.offHint")) : null,
1517
+ row.status === "broken-link" ? h("div", { className: "dshsc-note warn" }, t("status.brokenHint")) : null
1518
+ ),
1519
+ h("div", { className: "dshsc-agents" }, agentChips(row)),
1520
+ h(
1521
+ "div",
1522
+ { className: "dshsc-actions" },
1523
+ h("button", {
1524
+ className: "dshsc-switch",
1525
+ role: "switch",
1526
+ "aria-checked": String(tracked && row.mode === "on"),
1527
+ "aria-label": `${row.name} ${t("mode.on")}/${t("mode.off")}`,
1528
+ // 未纳入追踪时调 setMode 只会报「没有安装记录」,不如直接禁用
1529
+ disabled: busy !== null || !tracked,
1530
+ onClick: () => void actions.onToggleMode(row),
1531
+ title: tracked ? row.mode === "on" ? t("mode.off") : t("mode.on") : t("adopt.switchHint")
1532
+ }, h("span", { className: "dshsc-knob" })),
1533
+ row.status === "unmanaged" ? h("button", {
1534
+ className: "dshsc-btn ghost",
1535
+ disabled: !writable || busy !== null,
1536
+ title: row.source ? row.source : t("meta.none"),
1537
+ onClick: () => void actions.onAdopt(row)
1538
+ }, t("action.adopt")) : h("button", {
1539
+ className: `dshsc-btn ghost${canUpdate ? " dshsc-update" : ""}`,
1540
+ disabled: !canUpdate || busy !== null,
1541
+ onClick: () => actions.onUpdate(row)
1542
+ }, t("action.update")),
1543
+ h("button", { className: "dshsc-btn ghost", disabled: busy !== null, onClick: () => actions.onLinks(row) }, t("action.links")),
1544
+ h("button", {
1545
+ className: "dshsc-btn ghost danger sq",
1546
+ disabled: !writable || busy !== null,
1547
+ title: t("action.delete"),
1548
+ "aria-label": `${t("action.delete")} ${row.name}`,
1549
+ onClick: () => actions.onDelete(row)
1550
+ }, h(TrashIcon, { size: 13 }))
1551
+ )
1552
+ );
1553
+ }
1554
+ function groupedRows() {
1555
+ const groups = [];
1556
+ const hasLink = (row, prefix) => row.links.some((link) => !link.broken && String(link.target).startsWith(prefix));
1557
+ const dshRows = visibleRows.filter((row) => row.mode === "on" && !row.scopeProjects?.length && hasLink(row, "dsh-global"));
1558
+ const projectRows = visibleRows.filter((row) => row.mode === "on" && row.scopeProjects?.length);
1559
+ const offRows = visibleRows.filter((row) => row.mode === "off");
1560
+ const unlinkedRows = visibleRows.filter((row) => row.mode === "on" && !row.scopeProjects?.length && !row.links.some((link) => !link.broken));
1561
+ groups.push({ key: "dsh-global", title: t("group.dshGlobal"), rows: dshRows, dot: true });
1562
+ if (projectRows.length) groups.push({ key: "dsh-project", title: t("group.dshProject"), rows: projectRows, dot: true });
1563
+ if (offRows.length) groups.push({ key: "off", title: t("group.off"), rows: offRows });
1564
+ if (unlinkedRows.length) groups.push({ key: "unlinked", title: t("group.unlinked"), rows: unlinkedRows });
1565
+ for (const agent of data?.agents?.detected || []) {
1566
+ if (agent.kind !== "agent") continue;
1567
+ const matched = visibleRows.filter((row) => row.links.some((link) => link.target === agent.target));
1568
+ if (matched.length) groups.push({ key: agent.target, title: agent.label, rows: matched });
1569
+ }
1570
+ return groups.filter((group) => group.rows.length > 0);
1571
+ }
1572
+ function filterChip(id, label, dot) {
1573
+ return h("button", {
1574
+ key: id,
1575
+ type: "button",
1576
+ className: `dshsc-fchip${filter === id ? " active" : ""}`,
1577
+ "aria-pressed": String(filter === id),
1578
+ title: id === "unknown" && counts.unknown ? t("hint.unknown").replace("{n}", String(counts.unknown)) : void 0,
1579
+ onClick: () => actions.onFilterChange(id)
1580
+ }, dot, h("span", null, label), h("span", { className: "n" }, counts[id]));
1581
+ }
1582
+ function listBar() {
1583
+ return h(
1584
+ "div",
1585
+ { className: "dshsc-listbar" },
1586
+ h(
1587
+ "div",
1588
+ { className: "dshsc-filters" },
1589
+ filterChip("all", t("filter.all")),
1590
+ filterChip("update", t("filter.update"), h("i", { className: "dshsc-fdot" })),
1591
+ filterChip("unknown", t("filter.unknown")),
1592
+ filterChip("off", t("filter.off"))
1593
+ ),
1594
+ h(
1595
+ "div",
1596
+ { className: "dshsc-listbar-actions" },
1597
+ h("button", {
1598
+ className: "dshsc-btn sq",
1599
+ disabled: busy !== null,
1600
+ title: t("action.checkUpdates"),
1601
+ "aria-label": t("action.checkUpdates"),
1602
+ onClick: () => void actions.onCheckUpdates()
1603
+ }, h(RefreshIcon, { size: 14 })),
1604
+ bulkCount > 0 ? h("button", { className: "dshsc-btn tint", disabled: !writable || busy !== null, onClick: () => void actions.onUpdateAll() }, `${t("action.updateAll")} · ${bulkCount}`) : null
1605
+ )
1606
+ );
1607
+ }
1608
+ function filteredEmpty() {
1609
+ return h(
1610
+ "div",
1611
+ { className: "dshsc-empty" },
1612
+ h("div", { className: "dshsc-name" }, t("empty.filtered")),
1613
+ h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.filteredHint")),
1614
+ h(
1615
+ "div",
1616
+ { style: { marginTop: 10 } },
1617
+ h("button", {
1618
+ className: "dshsc-btn",
1619
+ onClick: actions.onClearFilters
1620
+ }, t("search.clear"))
1621
+ )
1622
+ );
1623
+ }
1624
+ function builtinNotice() {
1625
+ const items = (data?.visibleOnly || []).filter((item) => item.source === "bundled");
1626
+ if (items.length === 0) return null;
1627
+ const listId = "dshsc-builtin-list";
1628
+ return h(
1629
+ "div",
1630
+ { className: "dshsc-notice" },
1631
+ h(
1632
+ "div",
1633
+ { className: "dshsc-notice-row" },
1634
+ h(InfoIcon, { size: 13 }),
1635
+ h(
1636
+ "span",
1637
+ { className: "dshsc-notice-text" },
1638
+ t("visibleOnlyBuiltin"),
1639
+ " ",
1640
+ h("button", {
1641
+ className: "dshsc-linkbtn",
1642
+ "aria-expanded": String(builtinOpen),
1643
+ "aria-controls": builtinOpen ? listId : void 0,
1644
+ onClick: actions.onToggleBuiltin
1645
+ }, builtinOpen ? t("builtin.hide") : t("builtin.view").replace("{n}", String(items.length)))
1646
+ )
1647
+ ),
1648
+ builtinOpen ? h("div", { className: "dshsc-notice-list", id: listId }, items.map((item) => item.name).join("、")) : null
1649
+ );
1650
+ }
1651
+ function visibleOnlyBanners() {
1652
+ const others = (data?.visibleOnly || []).filter((item) => item.source !== "bundled");
1653
+ return others.length > 0 ? h(
1654
+ "div",
1655
+ { className: "dshsc-banner warn" },
1656
+ `${t("visibleOnlyOther")}:${others.map((item) => item.name).join("、")}`
1657
+ ) : null;
1658
+ }
1659
+ function content() {
1660
+ if (!data) return h("div", { className: "dshsc-empty" }, t("empty.loading"));
1661
+ if (rows.length === 0) {
1662
+ return h(
1663
+ "div",
1664
+ { className: "dshsc-empty" },
1665
+ h("div", { className: "dshsc-name" }, t("empty.skills")),
1666
+ h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.skillsHint")),
1667
+ h(
1668
+ "div",
1669
+ { style: { marginTop: 10 } },
1670
+ h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: actions.onAddSkill }, t("action.addSkill"))
1671
+ )
1672
+ );
1673
+ }
1674
+ const body = view === "agent" ? h("div", { className: "dshsc-groupstack" }, groupedRows().map((group) => h(
1675
+ "div",
1676
+ { key: group.key },
1677
+ h(
1678
+ "div",
1679
+ { className: "dshsc-group-head" },
1680
+ group.dot ? h("i", { className: "dshsc-gdot" }) : null,
1681
+ h("span", null, group.title),
1682
+ h("span", { className: "n" }, group.rows.length)
1683
+ ),
1684
+ h("div", { className: "dshsc-list" }, group.rows.map(rowView))
1685
+ ))) : h("div", { className: "dshsc-list" }, visibleRows.map(rowView));
1686
+ return h(
1687
+ "div",
1688
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
1689
+ visibleOnlyBanners(),
1690
+ builtinNotice(),
1691
+ listBar(),
1692
+ visibleRows.length === 0 ? filteredEmpty() : body
1693
+ );
1694
+ }
1695
+ return content();
1696
+ }
1697
+
1698
+ // client/views/agent-tip.js
1699
+ function AgentTip({ t, agentTip, agentTipPos, nodeRef }) {
1700
+ const node = h(
1701
+ "div",
1702
+ {
1703
+ ref: nodeRef,
1704
+ id: "dshsc-agenttip",
1705
+ role: "tooltip",
1706
+ className: "dshsc-agenttip",
1707
+ style: agentTipPos ? { left: `${agentTipPos.left}px`, top: `${agentTipPos.top}px` } : { left: 0, top: 0, visibility: "hidden" }
1708
+ },
1709
+ h(
1710
+ "div",
1711
+ { className: "dshsc-agenttip-head" },
1712
+ h("span", null, t("meta.agentsAll")),
1713
+ h("span", { className: "dshsc-agenttip-n" }, agentTip.labels.length)
1714
+ ),
1715
+ h(
1716
+ "div",
1717
+ { className: "dshsc-agenttip-list" },
1718
+ agentTip.labels.map((label, index) => h("span", {
1719
+ className: `dshsc-agent${label === "DSH" ? " dsh" : ""}`,
1720
+ key: `agenttip-${index}`
1721
+ }, label === "DSH" ? h("i", { className: "dshsc-dot" }) : null, label))
1722
+ )
1723
+ );
1724
+ return portalize(node);
1725
+ }
1726
+
1727
+ // client/views/subscriptions.js
1728
+ function SubscriptionsTab({
1729
+ t,
1730
+ subscriptions,
1731
+ rows,
1732
+ expanded,
1733
+ busy,
1734
+ writable,
1735
+ onToggle,
1736
+ onRefresh,
1737
+ onDelete,
1738
+ onInstall,
1739
+ onAdd
1740
+ }) {
1741
+ if (subscriptions.length === 0) {
1742
+ return h(
1743
+ "div",
1744
+ { className: "dshsc-empty" },
1745
+ h("div", { className: "dshsc-name" }, t("empty.subs")),
1746
+ h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.subsHint")),
1747
+ h(
1748
+ "div",
1749
+ { style: { marginTop: 10 } },
1750
+ h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: onAdd }, t("action.addSubscription"))
1751
+ )
1752
+ );
1753
+ }
1754
+ return h("div", { className: "dshsc-list" }, subscriptions.map((subscription) => {
1755
+ const entries = subscription.snapshot?.entries || [];
1756
+ const installedNames = new Set(rows.filter((row) => row.subscriptionId === subscription.id).map((row) => row.name));
1757
+ const open = Boolean(expanded[subscription.id]);
1758
+ return h(
1759
+ "div",
1760
+ { className: "dshsc-sub-block", key: subscription.id },
1761
+ h(
1762
+ "div",
1763
+ { className: "dshsc-sub-row" },
1764
+ h("button", {
1765
+ className: "dshsc-caret",
1766
+ "aria-expanded": String(open),
1767
+ "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
1768
+ onClick: () => onToggle(subscription.id)
1769
+ }, h(CaretIcon, { open })),
1770
+ h("span", { className: "dshsc-name" }, subscription.source),
1771
+ h("span", { className: "dshsc-tag" }, subscription.ref || "default"),
1772
+ h(
1773
+ "span",
1774
+ { className: "dshsc-substat" },
1775
+ `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedNames.size}`
1776
+ ),
1777
+ h(
1778
+ "div",
1779
+ { className: "dshsc-actions" },
1780
+ h("button", {
1781
+ className: "dshsc-btn sq",
1782
+ disabled: busy !== null,
1783
+ title: t("action.checkUpdates"),
1784
+ "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
1785
+ onClick: () => void onRefresh(subscription)
1786
+ }, h(RefreshIcon, { size: 13 })),
1787
+ h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
1788
+ )
1789
+ ),
1790
+ open ? h("div", { className: "dshsc-sub-list" }, entries.map((entry) => {
1791
+ const installedAlready = installedNames.has(entry.name);
1792
+ return h(
1793
+ "div",
1794
+ { className: "dshsc-sub-entry", key: entry.name },
1795
+ h("span", { className: "dshsc-name" }, entry.name),
1796
+ h(
1797
+ "span",
1798
+ { className: `dshsc-state${installedAlready ? " ok" : ""}` },
1799
+ installedAlready ? "✓ " : "",
1800
+ installedAlready ? t("installed") : t("notInstalled")
1801
+ ),
1802
+ h(
1803
+ "div",
1804
+ { className: "dshsc-actions" },
1805
+ installedAlready ? null : h("button", {
1806
+ className: "dshsc-btn",
1807
+ disabled: !writable || busy !== null,
1808
+ onClick: () => void onInstall(subscription, entry)
1809
+ }, t("action.install"))
1810
+ )
1811
+ );
1812
+ })) : null
1813
+ );
1814
+ }));
1815
+ }
1816
+
1817
+ // client/views/modals.js
1818
+ var import_react3 = __toESM(require("react"), 1);
1819
+ function Overlay({ t, title, children, footer, onCancel }) {
1820
+ import_react3.default.useEffect(() => {
1821
+ const onKey = (event) => {
1822
+ if (event.key === "Escape") onCancel?.();
1823
+ };
1824
+ window.addEventListener("keydown", onKey);
1825
+ return () => window.removeEventListener("keydown", onKey);
1826
+ }, [onCancel]);
1827
+ return h(
1828
+ "div",
1829
+ { className: "dshsc-overlay", onClick: (event) => {
1830
+ if (event.target === event.currentTarget) onCancel?.();
1831
+ } },
1832
+ h(
1833
+ "div",
1834
+ { className: "dshsc-modal", role: "dialog", "aria-modal": "true" },
1835
+ h("button", {
1836
+ className: "dshsc-modal-close",
1837
+ "aria-label": t("action.cancel"),
1838
+ onClick: (event) => {
1839
+ event.stopPropagation();
1840
+ onCancel?.();
1841
+ }
1842
+ }, "×"),
1843
+ h("h3", null, title),
1844
+ children,
1845
+ h("div", { className: "dshsc-modal-foot" }, footer)
1846
+ )
1847
+ );
1848
+ }
1849
+ function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
1850
+ const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
1851
+ return h(
1852
+ Overlay,
1853
+ {
1854
+ t,
1855
+ title,
1856
+ onCancel,
1857
+ footer: [
1858
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
1859
+ h("button", {
1860
+ key: "s",
1861
+ className: "dshsc-btn primary",
1862
+ disabled: !form[fields[0].key]?.trim?.() || fields.some((f) => f.key === "skills") && !form.skills?.trim(),
1863
+ onClick: () => onSubmit(form)
1864
+ }, t("action.save"))
1865
+ ]
1866
+ },
1867
+ hint ? h("p", { className: "dshsc-sub" }, hint) : null,
1868
+ ...fields.map((field) => h(
1869
+ "label",
1870
+ { className: "dshsc-field", key: field.key },
1871
+ h("span", null, field.label),
1872
+ h("input", {
1873
+ value: form[field.key],
1874
+ placeholder: field.placeholder,
1875
+ onChange: (event) => setForm((prev) => ({ ...prev, [field.key]: event.target.value }))
1876
+ })
1877
+ ))
1878
+ );
1879
+ }
1880
+ function ConfirmModal({ t, title, body, onCancel, onConfirm, danger, confirmLabel }) {
1881
+ return h(Overlay, {
1882
+ t,
1883
+ title,
1884
+ onCancel,
1885
+ footer: [
1886
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
1887
+ h("button", { key: "o", className: `dshsc-btn${danger ? " danger" : " primary"}`, onClick: onConfirm }, confirmLabel || t("action.save"))
1888
+ ]
1889
+ }, h("p", { className: "dshsc-sub", style: { whiteSpace: "pre-wrap" } }, body));
1890
+ }
1891
+ function LinksModal({ t, row, agents, workspaces, pickDirectory, onCancel, onSave }) {
1892
+ const candidates = agents.filter(
1893
+ (agent) => agent.kind === "dsh" || agent.kind !== "project" && agent.exists
1894
+ );
1895
+ const [targets, setTargets] = import_react3.default.useState(() => {
1896
+ const visible = new Set(candidates.map((agent) => agent.target));
1897
+ const initial = new Set(row.links.map((link) => link.target).filter((target) => visible.has(target)));
1898
+ initial.add("dsh-global");
1899
+ return initial;
1900
+ });
1901
+ const [scopeProjects, setScopeProjects] = import_react3.default.useState(row.scopeProjects || []);
1902
+ const [scopeMode, setScopeMode] = import_react3.default.useState((row.scopeProjects || []).length > 0 ? "projects" : "global");
1903
+ const workspacePaths = new Set(workspaces.map((workspace) => workspace.path));
1904
+ const otherProjects = scopeProjects.filter((project) => !workspacePaths.has(project));
1905
+ const toggle = (target) => setTargets((prev) => {
1906
+ const next = new Set(prev);
1907
+ if (next.has(target)) next.delete(target);
1908
+ else next.add(target);
1909
+ return next;
1910
+ });
1911
+ const toggleProject = (project) => setScopeProjects((prev) => prev.includes(project) ? prev.filter((item) => item !== project) : [...prev, project]);
1912
+ const chooseDirectory = async () => {
1913
+ const dir = await pickDirectory?.();
1914
+ if (!dir) return;
1915
+ setScopeProjects((prev) => prev.includes(dir) ? prev : [...prev, dir]);
1916
+ };
1917
+ return h(
1918
+ Overlay,
1919
+ {
1920
+ t,
1921
+ title: `${t("action.links")} · ${row.name}`,
1922
+ onCancel,
1923
+ footer: [
1924
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
1925
+ h("button", {
1926
+ key: "s",
1927
+ className: "dshsc-btn primary",
1928
+ onClick: () => onSave([...targets], scopeMode === "projects" ? scopeProjects.filter((item) => item.trim()) : [])
1929
+ }, t("action.save"))
1930
+ ]
1931
+ },
1932
+ h("div", { className: "dshsc-group" }, t("links.targets")),
1933
+ h("p", { className: "dshsc-sub" }, t("links.detectedHint")),
1934
+ h("p", { className: "dshsc-sub" }, t("links.canonical").replace("{path}", `~/.agents/skills/${row.name}`)),
1935
+ ...candidates.map((agent) => h(
1936
+ "label",
1937
+ { className: "dshsc-check", key: agent.target },
1938
+ h("input", {
1939
+ type: "checkbox",
1940
+ checked: agent.target === "dsh-global" ? true : targets.has(agent.target),
1941
+ disabled: agent.target === "dsh-global",
1942
+ onChange: () => toggle(agent.target)
377
1943
  }),
378
- h('circle', { cx: 18, cy: 17, r: 4.6, fill: '#7ef0c0' }),
379
- h('circle', { cx: 46, cy: 17, r: 4.6, fill: '#ffffff' }),
380
- h('circle', { cx: 32, cy: 49, r: 4.6, fill: '#ffffff' }),
381
- h('rect', { x: 21, y: 21, width: 22, height: 22, rx: 7, fill: '#ffffff' }),
382
- h('path', {
383
- d: 'M27 32.5l3.6 3.6 6.8-7.4',
384
- fill: 'none', stroke: '#1d4ed8', strokeWidth: 3, strokeLinecap: 'round', strokeLinejoin: 'round',
385
- }));
1944
+ h("span", null, agent.kind === "dsh" ? t("links.dshGlobal") : agent.label),
1945
+ agent.target === "dsh-global" ? h("small", null, t("links.required")) : null,
1946
+ h("small", { title: agent.dir }, agent.dir)
1947
+ )),
1948
+ h("div", { className: "dshsc-group" }, t("links.scope")),
1949
+ h(
1950
+ "label",
1951
+ { className: "dshsc-check" },
1952
+ h("input", { type: "radio", name: `scope-${row.name}`, checked: scopeMode === "global", onChange: () => setScopeMode("global") }),
1953
+ h("span", null, t("links.scopeGlobal"))
1954
+ ),
1955
+ h(
1956
+ "label",
1957
+ { className: "dshsc-check" },
1958
+ h("input", { type: "radio", name: `scope-${row.name}`, checked: scopeMode === "projects", onChange: () => setScopeMode("projects") }),
1959
+ h("span", null, t("links.scopeProjects"))
1960
+ ),
1961
+ scopeMode === "projects" ? h(
1962
+ "div",
1963
+ null,
1964
+ h("div", { className: "dshsc-group" }, t("links.workspaces")),
1965
+ workspaces.length === 0 ? h("p", { className: "dshsc-sub" }, t("links.noWorkspaces")) : h("div", { className: "dshsc-panel" }, workspaces.map((workspace) => h(
1966
+ "label",
1967
+ { className: "dshsc-check", key: workspace.id },
1968
+ h("input", {
1969
+ type: "checkbox",
1970
+ checked: scopeProjects.includes(workspace.path),
1971
+ onChange: () => toggleProject(workspace.path)
1972
+ }),
1973
+ h("span", null, workspace.title),
1974
+ h("small", null, workspace.path)
1975
+ ))),
1976
+ otherProjects.length > 0 ? h("div", { className: "dshsc-group" }, t("links.selected")) : null,
1977
+ ...otherProjects.map((project) => h(
1978
+ "div",
1979
+ { className: "dshsc-sub-entry", key: project },
1980
+ h("span", { className: "dshsc-code" }, project),
1981
+ h(
1982
+ "div",
1983
+ { className: "dshsc-actions" },
1984
+ h("button", {
1985
+ className: "dshsc-btn ghost danger",
1986
+ onClick: () => setScopeProjects((prev) => prev.filter((item) => item !== project))
1987
+ }, t("action.delete"))
1988
+ )
1989
+ )),
1990
+ h("button", { className: "dshsc-btn", style: { alignSelf: "flex-start", marginTop: 6 }, onClick: () => void chooseDirectory() }, t("links.pickDirectory"))
1991
+ ) : null,
1992
+ h("p", { className: "dshsc-sub" }, t("links.hint"))
1993
+ );
1994
+ }
1995
+ function AutoLinkModal({ t, settings, agents, onCancel, onSave }) {
1996
+ const [targets, setTargets] = import_react3.default.useState(() => new Set(
1997
+ Array.isArray(settings.autoLinkTargets) ? settings.autoLinkTargets : agents.map((agent) => agent.target)
1998
+ ));
1999
+ const toggle = (target) => setTargets((prev) => {
2000
+ const next = new Set(prev);
2001
+ if (next.has(target)) next.delete(target);
2002
+ else next.add(target);
2003
+ return next;
2004
+ });
2005
+ return h(
2006
+ Overlay,
2007
+ {
2008
+ t,
2009
+ title: t("autoLink"),
2010
+ onCancel,
2011
+ footer: [
2012
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2013
+ h("button", { key: "s", className: "dshsc-btn primary", onClick: () => onSave([...targets]) }, t("action.save"))
2014
+ ]
2015
+ },
2016
+ h("p", { className: "dshsc-sub" }, t("links.dshGlobal") + " · " + t("links.required")),
2017
+ h("div", { className: "dshsc-group" }, t("autoLink.detected")),
2018
+ agents.length === 0 ? h("p", { className: "dshsc-sub" }, t("autoLink.none")) : h("div", { className: "dshsc-panel" }, agents.map((agent) => h(
2019
+ "label",
2020
+ { className: "dshsc-check", key: agent.target },
2021
+ h("input", {
2022
+ type: "checkbox",
2023
+ checked: targets.has(agent.target),
2024
+ onChange: () => toggle(agent.target)
2025
+ }),
2026
+ h("span", null, agent.label),
2027
+ h("small", null, agent.dir)
2028
+ ))),
2029
+ h("p", { className: "dshsc-sub" }, t("autoLink.hint"))
2030
+ );
2031
+ }
2032
+ function Modals({ t, modal, data, autoLinkAgents, pickDirectory, actions, onClose, onSaveAutoLink }) {
2033
+ if (modal.type === "add-skill") {
2034
+ return h(FormModal, {
2035
+ t,
2036
+ title: t("action.addSkill"),
2037
+ hint: t("form.hint"),
2038
+ fields: [
2039
+ { key: "source", label: t("form.source"), placeholder: t("form.sourceHint") },
2040
+ { key: "skills", label: t("form.skills"), placeholder: "git-kit, handoff" }
2041
+ ],
2042
+ onCancel: onClose,
2043
+ onSubmit: (form) => void actions.addSkill(form)
2044
+ });
2045
+ }
2046
+ if (modal.type === "add-subscription") {
2047
+ return h(FormModal, {
2048
+ t,
2049
+ title: t("action.addSubscription"),
2050
+ hint: t("form.subHint"),
2051
+ fields: [
2052
+ { key: "source", label: t("form.source"), placeholder: t("form.sourceHint") },
2053
+ { key: "ref", label: t("form.ref"), placeholder: "main" },
2054
+ { key: "subpath", label: t("form.subpath"), placeholder: "skills" }
2055
+ ],
2056
+ onCancel: onClose,
2057
+ onSubmit: (form) => void actions.addSubscription(form)
2058
+ });
2059
+ }
2060
+ if (modal.type === "links") {
2061
+ return h(LinksModal, {
2062
+ t,
2063
+ row: modal.row,
2064
+ agents: data?.agents?.all || [],
2065
+ workspaces: data?.workspaces || [],
2066
+ pickDirectory,
2067
+ onCancel: onClose,
2068
+ onSave: (targets, scopeProjects) => {
2069
+ onClose();
2070
+ void actions.saveLinks(modal.row, targets, scopeProjects);
2071
+ }
2072
+ });
2073
+ }
2074
+ if (modal.type === "autolink") {
2075
+ return h(AutoLinkModal, {
2076
+ t,
2077
+ settings: data?.settings || {},
2078
+ agents: autoLinkAgents,
2079
+ onCancel: onClose,
2080
+ onSave: (targets) => void onSaveAutoLink(targets)
2081
+ });
2082
+ }
2083
+ if (modal.type === "confirm-delete") {
2084
+ return h(ConfirmModal, {
2085
+ t,
2086
+ title: t("confirm.deleteTitle"),
2087
+ body: t("confirm.deleteBody"),
2088
+ danger: true,
2089
+ onCancel: onClose,
2090
+ onConfirm: modal.onConfirm
2091
+ });
2092
+ }
2093
+ if (modal.type === "confirm-update") {
2094
+ return h(ConfirmModal, {
2095
+ t,
2096
+ title: t("confirm.updateTitle"),
2097
+ body: t("confirm.updateBody"),
2098
+ onCancel: onClose,
2099
+ onConfirm: modal.onConfirm
2100
+ });
2101
+ }
2102
+ if (modal.type === "conflict") {
2103
+ const conflicts = modal.detail?.conflicts || [];
2104
+ return h(ConfirmModal, {
2105
+ t,
2106
+ title: t("error.conflictTitle"),
2107
+ body: `${modal.message}
2108
+ ${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join("\n")}`,
2109
+ onCancel: onClose,
2110
+ onConfirm: onClose,
2111
+ confirmLabel: t("action.cancel")
2112
+ });
2113
+ }
2114
+ return null;
2115
+ }
2116
+
2117
+ // client/section.js
2118
+ var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
2119
+ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2120
+ const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
2121
+ const [tab, setTab] = import_react4.default.useState("installed");
2122
+ const [view, setView] = import_react4.default.useState("flat");
2123
+ const [modal, setModal] = import_react4.default.useState(null);
2124
+ const [expanded, setExpanded] = import_react4.default.useState({});
2125
+ const [placementBusy, setPlacementBusy] = import_react4.default.useState(null);
2126
+ const [placementOverride, setPlacementOverride] = import_react4.default.useState(null);
2127
+ const [query, setQuery] = import_react4.default.useState("");
2128
+ const [filter, setFilter] = import_react4.default.useState("all");
2129
+ const [builtinOpen, setBuiltinOpen] = import_react4.default.useState(false);
2130
+ const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
2131
+ const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
2132
+ const rows = data?.installs || [];
2133
+ const actions = useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal });
2134
+ const writable = Boolean(data) && !data?.readOnly && data?.cli?.usable;
2135
+ const updatable = (row) => UPDATABLE2.has(row.status);
2136
+ const updateCount = rows.filter(updatable).length;
2137
+ const bulkCount = rows.filter((row) => row.status === "update-available").length;
2138
+ const unknownCount = rows.filter((row) => row.status === "unknown").length;
2139
+ const offCount = rows.filter((row) => row.mode === "off").length;
2140
+ const counts = { all: rows.length, update: updateCount, unknown: unknownCount, off: offCount };
2141
+ const queryText = query.trim().toLowerCase();
2142
+ const visibleRows = rows.filter((row) => {
2143
+ if (filter === "update" && !updatable(row)) return false;
2144
+ if (filter === "unknown" && row.status !== "unknown") return false;
2145
+ if (filter === "off" && row.mode !== "off") return false;
2146
+ if (!queryText) return true;
2147
+ const agentLabels = row.links.map((link) => link.target === "dsh-global" ? "DSH" : link.label);
2148
+ return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
2149
+ });
2150
+ const autolinkTargets = (data?.agents?.detected || []).filter((agent) => agent.kind === "agent");
2151
+ const autoLinkSelection = data?.settings?.autoLinkTargets;
2152
+ const autoLinkAgents = Array.isArray(autoLinkSelection) ? autolinkTargets.filter((agent) => autoLinkSelection.includes(agent.target)) : autolinkTargets;
2153
+ const autoLinkLabel = [t("links.dshGlobal"), ...autoLinkAgents.map((agent) => agent.label)].join("、");
2154
+ const placement = (placementOverride ?? data?.settings?.placement) === "sidebar" ? "sidebar" : "settings";
2155
+ async function changePlacement(next) {
2156
+ if (next === placement || placementBusy) return;
2157
+ setPlacementBusy(next);
2158
+ try {
2159
+ await apiCall("POST", "/settings", { placement: next });
2160
+ setPlacementOverride(next);
2161
+ closeSettings();
2162
+ onPlacementChange?.(next);
2163
+ setPlacementBusy(null);
2164
+ } catch (err) {
2165
+ setToast({ kind: "err", text: err.message });
2166
+ setPlacementBusy(null);
386
2167
  }
387
-
388
- function PanelIcon({ size = 16 }) {
389
- return h('svg', {
390
- width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
391
- strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true,
392
- style: { display: 'block' },
2168
+ }
2169
+ async function saveAutoLink(targets) {
2170
+ setModal(null);
2171
+ await run("settings", () => apiCall("POST", "/settings", { autoLinkTargets: targets }));
2172
+ }
2173
+ const installedActions = {
2174
+ onAdopt: actions.adopt,
2175
+ onToggleMode: actions.toggleMode,
2176
+ onUpdate: actions.requestUpdate,
2177
+ onDelete: actions.requestDelete,
2178
+ onLinks: (row) => setModal({ type: "links", row }),
2179
+ onCheckUpdates: actions.checkUpdates,
2180
+ onUpdateAll: actions.updateAll,
2181
+ onAddSkill: actions.requestAddSkill,
2182
+ onFilterChange: setFilter,
2183
+ onQueryChange: setQuery,
2184
+ onOpenAgentTip: openAgentTip,
2185
+ onCloseAgentTip: closeAgentTip,
2186
+ onToggleBuiltin: () => setBuiltinOpen((prev) => !prev),
2187
+ onClearFilters: () => {
2188
+ setQuery("");
2189
+ setFilter("all");
2190
+ }
2191
+ };
2192
+ const subscriptionActions = {
2193
+ onToggle: (id) => setExpanded((prev) => ({ ...prev, [id]: !prev[id] })),
2194
+ onRefresh: actions.refreshSubscription,
2195
+ onDelete: actions.deleteSubscription,
2196
+ onInstall: actions.installFromSubscription,
2197
+ onAdd: actions.requestAddSubscription
2198
+ };
2199
+ const root = h(
2200
+ "div",
2201
+ { className: "dshsc-root" },
2202
+ h("style", null, CSS),
2203
+ h(Header, {
2204
+ t,
2205
+ page,
2206
+ plugin: data?.plugin,
2207
+ writable,
2208
+ autoLinkLabel,
2209
+ autoLinkCount: autoLinkAgents.length + 1,
2210
+ tab,
2211
+ settingsOpen,
2212
+ settingsRef,
2213
+ settingsPlacement,
2214
+ placement,
2215
+ placementBusy,
2216
+ readOnly: Boolean(data?.readOnly),
2217
+ onToggleSettings: toggleSettings,
2218
+ onChangePlacement: (next) => void changePlacement(next),
2219
+ onOpenAutoLink: () => setModal({ type: "autolink" }),
2220
+ onAddSkill: actions.requestAddSkill,
2221
+ onAddSubscription: actions.requestAddSubscription
2222
+ }),
2223
+ error ? h("div", { className: "dshsc-banner err" }, `${error} · `, h("button", { className: "dshsc-btn ghost", onClick: () => void reload() }, t("action.retry"))) : null,
2224
+ data?.readOnly ? h("div", { className: "dshsc-banner warn" }, t("banner.readonly")) : null,
2225
+ data && !data.cli?.usable ? h("div", { className: "dshsc-banner err" }, `${t("banner.cli")} · ${data.cli?.source || ""} ${data.cli?.version || ""}`) : null,
2226
+ h(Toolbar, {
2227
+ t,
2228
+ tab,
2229
+ onTabChange: setTab,
2230
+ installedCount: rows.length,
2231
+ subscriptionCount: data?.subscriptions?.length || 0,
2232
+ query,
2233
+ onQueryChange: setQuery,
2234
+ view,
2235
+ onViewChange: setView
2236
+ }),
2237
+ tab === "installed" ? h(InstalledTab, {
2238
+ t,
2239
+ data,
2240
+ rows,
2241
+ visibleRows,
2242
+ counts,
2243
+ bulkCount,
2244
+ filter,
2245
+ query,
2246
+ view,
2247
+ busy,
2248
+ writable,
2249
+ agentTip,
2250
+ builtinOpen,
2251
+ actions: installedActions
2252
+ }) : h(SubscriptionsTab, {
2253
+ t,
2254
+ subscriptions: data?.subscriptions || [],
2255
+ rows,
2256
+ expanded,
2257
+ busy,
2258
+ writable,
2259
+ ...subscriptionActions
2260
+ }),
2261
+ busy ? h("div", { className: "dshsc-meta" }, `${t("busy.working")} ${busy}`) : null,
2262
+ toast ? h("div", { className: `dshsc-toast ${toast.kind}` }, toast.text) : null,
2263
+ modal ? h(Modals, {
2264
+ t,
2265
+ modal,
2266
+ data,
2267
+ autoLinkAgents: autolinkTargets,
2268
+ pickDirectory,
2269
+ actions: {
2270
+ addSkill: actions.addSkill,
2271
+ addSubscription: actions.addSubscription,
2272
+ saveLinks: actions.saveLinks
393
2273
  },
394
- h('rect', { x: 7.5, y: 7.5, width: 9, height: 9, rx: 3 }),
395
- h('path', { d: 'M9.8 12.1l1.5 1.5 3.1-3.3' }),
396
- h('path', { d: 'M12 7.5V4.8M12 16.5v2.7M7.5 12H4.8', opacity: 0.55 }),
397
- h('circle', { cx: 12, cy: 4.2, r: 1.1, fill: 'currentColor', stroke: 'none' }),
398
- h('circle', { cx: 12, cy: 19.8, r: 1.1, fill: 'currentColor', stroke: 'none' }),
399
- h('circle', { cx: 4.2, cy: 12, r: 1.1, fill: 'currentColor', stroke: 'none' }));
2274
+ onClose: () => setModal(null),
2275
+ onSaveAutoLink: saveAutoLink
2276
+ }) : null,
2277
+ agentTip ? h(AgentTip, { t, agentTip, agentTipPos, nodeRef: agentTipRef }) : null
2278
+ );
2279
+ return page ? h("div", { className: "dshsc-page" }, root) : root;
2280
+ }
2281
+
2282
+ // client/surface.js
2283
+ var PANEL_ID = "dsh-skills-console";
2284
+ function createSurface({ slots, layout, t, pickDirectory }) {
2285
+ let surface = null;
2286
+ let userSelected = false;
2287
+ const surfaceDisposers = [];
2288
+ function SettingsSurface(props) {
2289
+ return h(Section, {
2290
+ t,
2291
+ page: false,
2292
+ pickDirectory,
2293
+ onPlacementChange: (next) => changeSurface(next, props?.close)
2294
+ });
2295
+ }
2296
+ function PageSurface() {
2297
+ return h(Section, { t, page: true, pickDirectory, onPlacementChange: (next) => changeSurface(next) });
2298
+ }
2299
+ function SurfaceIcon(props) {
2300
+ return h(PanelIcon, { size: props?.size || 16 });
2301
+ }
2302
+ function clearSurface() {
2303
+ while (surfaceDisposers.length > 0) {
2304
+ const dispose2 = surfaceDisposers.pop();
2305
+ try {
2306
+ dispose2();
2307
+ } catch {
2308
+ }
400
2309
  }
401
-
402
- function GitHubIcon({ size = 16 }) {
403
- return h('svg', {
404
- width: size, height: size, viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': true,
405
- style: { display: 'block' },
406
- }, h('path', {
407
- d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.809 1.305 3.495.998.108-.776.418-1.305.762-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
2310
+ }
2311
+ function mount(next) {
2312
+ const target = next === "sidebar" ? "sidebar" : "settings";
2313
+ if (surface === target) return;
2314
+ clearSurface();
2315
+ surface = target;
2316
+ if (target === "sidebar") {
2317
+ surfaceDisposers.push(slots.inject("main", () => {
2318
+ const disposeMain = slots.register({ name: "main", key: PANEL_ID }, PageSurface);
2319
+ const disposeIcon = slots.inject("sidebar.panellist", () => slots.register({
2320
+ name: "sidebar.panellist",
2321
+ id: PANEL_ID,
2322
+ order: 30,
2323
+ label: () => t("page.title")
2324
+ }, SurfaceIcon));
2325
+ return [disposeMain, disposeIcon];
408
2326
  }));
2327
+ } else {
2328
+ surfaceDisposers.push(slots.inject("settings.section", () => slots.register({
2329
+ name: "settings.section",
2330
+ id: PANEL_ID,
2331
+ order: 26,
2332
+ label: () => t("section.title")
2333
+ }, SettingsSurface)));
409
2334
  }
410
-
411
- return {
412
- inject: ['slots'],
413
- apply(ctx) {
414
- const locale = ctx.get('locale');
415
- const t = buildTranslate(locale);
416
- const slots = ctx.get('slots');
417
- if (!slots?.register) return;
418
-
419
- function Section(props) {
420
- const page = Boolean(props?.page);
421
- const [data, setData] = React.useState(null);
422
- const [error, setError] = React.useState(null);
423
- const [tab, setTab] = React.useState('installed');
424
- const [view, setView] = React.useState('flat');
425
- const [busy, setBusy] = React.useState(null);
426
- const [toast, setToast] = React.useState(null);
427
- const [modal, setModal] = React.useState(null);
428
- const [expanded, setExpanded] = React.useState({});
429
- const [placementBusy, setPlacementBusy] = React.useState(null);
430
-
431
- const reload = React.useCallback(async () => {
432
- try {
433
- const next = await apiCall('GET', '/state');
434
- setData(next);
435
- setError(null);
436
- } catch (err) {
437
- setError(err.message);
438
- }
439
- }, []);
440
-
441
- React.useEffect(() => {
442
- void reload();
443
- }, [reload]);
444
-
445
- React.useEffect(() => {
446
- if (!toast) return undefined;
447
- const timer = setTimeout(() => setToast(null), 4200);
448
- return () => clearTimeout(timer);
449
- }, [toast]);
450
-
451
- const run = React.useCallback(
452
- async (label, task) => {
453
- setBusy(label);
454
- try {
455
- const result = await task();
456
- await reload();
457
- return result;
458
- } catch (err) {
459
- setToast({ kind: 'err', text: err.message });
460
- throw err;
461
- } finally {
462
- setBusy(null);
463
- }
464
- },
465
- [reload],
466
- );
467
-
468
- const rows = data?.installs || [];
469
- const writable = Boolean(data) && !data?.readOnly && data?.cli?.usable;
470
- const updateCount = rows.filter((row) => row.status === 'update-available' || row.status === 'conflict').length;
471
-
472
- async function adopt(row) {
473
- await run(`adopt:${row.name}`, async () => {
474
- const result = await apiCall('POST', `/skills/${encodeURIComponent(row.name)}/adopt`, {});
475
- setToast({ kind: 'ok', text: `${row.name} · ${result.tracked ? t('adopt.done') : t('adopt.noUpstream')}` });
476
- return result;
477
- });
478
- }
479
-
480
- async function toggleMode(row) {
481
- const mode = row.mode === 'on' ? 'off' : 'on';
482
- await run(`mode:${row.name}`, () => apiCall('POST', `/skills/${encodeURIComponent(row.name)}/mode`, { mode }));
483
- }
484
-
485
- async function updateOne(row, overwrite) {
486
- await run(`update:${row.name}`, async () => {
487
- const result = await apiCall('POST', '/updates/apply', { name: row.name, overwrite });
488
- setToast({ kind: 'ok', text: `${row.name} → ${short(result.toCommit) || 'latest'}` });
489
- return result;
490
- });
491
- }
492
-
493
- function requestUpdate(row) {
494
- if (row.status === 'locally-modified' || row.status === 'conflict') {
495
- setModal({
496
- type: 'confirm-update',
497
- row,
498
- onConfirm: () => {
499
- setModal(null);
500
- void updateOne(row, true);
501
- },
502
- });
503
- return;
504
- }
505
- void updateOne(row, false);
506
- }
507
-
508
- async function updateAll() {
509
- const pending = rows.filter((row) => row.status === 'update-available');
510
- for (const row of pending) {
511
- try {
512
- await run(`update:${row.name}`, () => apiCall('POST', '/updates/apply', { name: row.name }));
513
- } catch {
514
- // 单条失败不阻断其余
515
- }
516
- }
517
- setToast({ kind: 'ok', text: t('action.updateAll') });
518
- }
519
-
520
- async function checkUpdates() {
521
- await run('check', async () => {
522
- const result = await apiCall('POST', '/updates/check', {});
523
- setToast({ kind: 'ok', text: `${t('toast.checked')} · ${result.checks.length}` });
524
- });
525
- }
526
-
527
- function requestDelete(row) {
528
- setModal({
529
- type: 'confirm-delete',
530
- row,
531
- onConfirm: async () => {
532
- setModal(null);
533
- setBusy(`delete:${row.name}`);
534
- try {
535
- await apiCall('DELETE', `/skills/${encodeURIComponent(row.name)}`);
536
- setToast({ kind: 'ok', text: `${row.name} · ${t('action.delete')} ✓` });
537
- await reload();
538
- } catch (err) {
539
- if (err.code === 'error.skills.remove-conflict') {
540
- setModal({ type: 'conflict', row, detail: err.detail, message: err.message });
541
- } else {
542
- setToast({ kind: 'err', text: err.message });
543
- }
544
- } finally {
545
- setBusy(null);
546
- }
547
- },
548
- });
549
- }
550
-
551
- async function saveLinks(row, targets, scopeProjects) {
552
- const current = new Set(row.links.map((link) => link.target));
553
- // 作用范围非空 = 只写项目链,全局链要被摘掉(两条同时存在没有额外效果)
554
- const desired = new Set(scopeProjects.length > 0 ? targets.filter((target) => target !== 'dsh-global') : targets);
555
- const toRemove = [...current].filter((target) => !desired.has(target));
556
- await run(`links:${row.name}`, async () => {
557
- if (toRemove.length > 0) {
558
- await apiCall('POST', '/links', { skill: row.name, targets: toRemove, action: 'remove' });
559
- }
560
- await apiCall('POST', '/links', { skill: row.name, targets: [...desired], action: 'add', scopeProjects });
561
- });
562
- setToast({ kind: 'ok', text: `${row.name} · ${t('action.links')} ✓` });
563
- }
564
-
565
- function requestAddSkill() {
566
- setModal({ type: 'add-skill' });
567
- }
568
-
569
- function requestAddSubscription() {
570
- setModal({ type: 'add-subscription' });
571
- }
572
-
573
- async function addSkill(form) {
574
- const names = form.skills
575
- .split(/[,,\s]+/)
576
- .map((item) => item.trim())
577
- .filter(Boolean);
578
- setModal(null);
579
- await run('install', async () => {
580
- const result = await apiCall('POST', '/install', { source: form.source.trim(), skills: names });
581
- const warnings = result.warnings?.length || 0;
582
- setToast({
583
- kind: warnings ? 'err' : 'ok',
584
- text: `${result.installed?.length || 0} ${t('skillCount')}${warnings ? ` · ${warnings} 条提示` : ''}`,
585
- });
586
- });
587
- }
588
-
589
- async function addSubscription(form) {
590
- setModal(null);
591
- await run('subscribe', async () => {
592
- const result = await apiCall('POST', '/subscriptions', {
593
- source: form.source.trim(),
594
- ref: form.ref.trim() || undefined,
595
- subpath: form.subpath.trim() || undefined,
596
- });
597
- setToast({ kind: 'ok', text: result.subscription?.source || form.source });
598
- });
599
- }
600
-
601
- async function refreshSubscription(row) {
602
- await run(`refresh:${row.id}`, async () => {
603
- const result = await apiCall('POST', `/subscriptions/${encodeURIComponent(row.id)}/refresh`, {});
604
- setToast({ kind: 'ok', text: `${row.source} · ${result.changed ? 'snapshot updated' : 'unchanged'}` });
605
- });
606
- }
607
-
608
- async function deleteSubscription(row) {
609
- await run(`unsubscribe:${row.id}`, async () => {
610
- await apiCall('DELETE', `/subscriptions/${encodeURIComponent(row.id)}`, { keepInstalled: true });
611
- setToast({ kind: 'ok', text: `${row.source} · ${t('action.delete')} ✓` });
612
- });
613
- }
614
-
615
- async function installFromSubscription(subscription, entry) {
616
- await run(`install:${entry.name}`, async () => {
617
- await apiCall('POST', '/install', { subscriptionId: subscription.id, skills: [entry.name] });
618
- setToast({ kind: 'ok', text: `${entry.name} ✓` });
619
- });
620
- }
621
-
622
- function statusCell(row) {
623
- const map = {
624
- current: ['status.current', 'ok'],
625
- 'update-available': ['status.update', 'warn'],
626
- 'locally-modified': ['status.local', 'warn'],
627
- conflict: ['status.conflict', 'warn'],
628
- 'upstream-deleted': ['status.deleted', 'warn'],
629
- 'broken-link': ['status.broken', 'err'],
630
- unmanaged: ['status.unmanaged', ''],
631
- unknown: ['status.unknown', ''],
632
- };
633
- const [key, cls] = map[row.status] || ['status.unknown', ''];
634
- return h('span', { className: `dshsc-status ${cls}` }, t(key));
635
- }
636
-
637
- function metaLine(row) {
638
- const parts = [];
639
- if (row.source) {
640
- const ref = row.commit ? `#${short(row.commit)}` : row.ref ? `#${row.ref}` : '';
641
- parts.push(`${t('meta.from')} ${row.source}${ref}`);
642
- }
643
- // 只按现场链接说「可用」,没有链接就别默认写 DSH
644
- const available = row.links
645
- .filter((link) => !link.broken)
646
- .map((link) => (link.target === 'dsh-global' ? 'DSH' : link.label));
647
- parts.push(available.length > 0 ? `${t('meta.available')}:${available.join('、')}` : t('meta.unlinked'));
648
- if (row.scopeProjects?.length) parts.push(`${t('meta.scope')}:${row.scopeProjects.map((p) => p.split('/').pop()).join('、')}`);
649
- return h('div', { className: 'dshsc-meta' }, parts.map((text, index) => h('span', { key: index }, text)));
650
- }
651
-
652
- function rowView(row) {
653
- const off = row.mode === 'off';
654
- const tracked = row.status !== 'unmanaged';
655
- const canUpdate = writable && (row.status === 'update-available' || row.status === 'conflict' || row.status === 'locally-modified');
656
- return h(
657
- 'div',
658
- { className: `dshsc-row${off ? ' off' : ''}`, key: row.name },
659
- h(
660
- 'div',
661
- { className: 'dshsc-row-head' },
662
- h('span', { className: 'dshsc-name' }, row.name),
663
- statusCell(row),
664
- h(
665
- 'div',
666
- { className: 'dshsc-actions' },
667
- h('button', {
668
- className: 'dshsc-switch',
669
- role: 'switch',
670
- 'aria-checked': String(tracked && row.mode === 'on'),
671
- 'aria-label': `${row.name} ${t('mode.on')}/${t('mode.off')}`,
672
- // 未纳入追踪时调 setMode 只会报「没有安装记录」,不如直接禁用
673
- disabled: busy !== null || !tracked,
674
- onClick: () => void toggleMode(row),
675
- title: tracked ? (row.mode === 'on' ? t('mode.off') : t('mode.on')) : t('adopt.switchHint'),
676
- }, h('span', { className: 'dshsc-knob' })),
677
- row.status === 'unmanaged'
678
- ? h('button', {
679
- className: 'dshsc-btn ghost',
680
- disabled: !writable || busy !== null,
681
- title: row.source ? row.source : t('meta.none'),
682
- onClick: () => void adopt(row),
683
- }, t('action.adopt'))
684
- : h('button', { className: 'dshsc-btn ghost', disabled: !canUpdate || busy !== null, onClick: () => requestUpdate(row) }, t('action.update')),
685
- h('button', { className: 'dshsc-btn ghost', disabled: busy !== null, onClick: () => setModal({ type: 'links', row }) }, t('action.links')),
686
- h('button', { className: 'dshsc-btn ghost danger', disabled: !writable || busy !== null, onClick: () => requestDelete(row) }, t('action.delete')),
687
- ),
688
- ),
689
- metaLine(row),
690
- off ? h('div', { className: 'dshsc-meta' }, t('mode.offHint')) : null,
691
- row.status === 'broken-link' ? h('div', { className: 'dshsc-meta' }, `⚠ ${t('status.broken')}`) : null,
692
- );
693
- }
694
-
695
- function groupedRows() {
696
- const groups = [];
697
- const dshRows = rows.filter((row) => row.mode === 'on' && !row.scopeProjects?.length);
698
- const projectRows = rows.filter((row) => row.mode === 'on' && row.scopeProjects?.length);
699
- const offRows = rows.filter((row) => row.mode === 'off');
700
- groups.push({ title: 'DSH(全局)', rows: dshRows });
701
- if (projectRows.length) groups.push({ title: 'DSH(仅特定项目)', rows: projectRows });
702
- if (offRows.length) groups.push({ title: '已停用', rows: offRows });
703
- for (const agent of data?.agents?.detected || []) {
704
- if (agent.kind !== 'agent') continue;
705
- const matched = rows.filter((row) => row.links.some((link) => link.target === agent.target));
706
- if (matched.length) groups.push({ title: agent.label, rows: matched });
707
- }
708
- return groups.filter((group) => group.rows.length > 0);
709
- }
710
-
711
- function installedTab() {
712
- if (!data) return h('div', { className: 'dshsc-empty' }, t('busy.working'));
713
- if (rows.length === 0) {
714
- return h(
715
- 'div',
716
- { className: 'dshsc-empty' },
717
- h('div', { className: 'dshsc-name' }, t('empty.skills')),
718
- h('div', { className: 'dshsc-meta', style: { justifyContent: 'center' } }, t('empty.skillsHint')),
719
- h('div', { style: { marginTop: 10 } },
720
- h('button', { className: 'dshsc-btn primary', disabled: !writable, onClick: requestAddSkill }, t('action.addSkill'))),
721
- );
722
- }
723
- const body =
724
- view === 'agent'
725
- ? groupedRows().map((group) =>
726
- h('div', { key: group.title }, h('div', { className: 'dshsc-group' }, group.title), h('div', { className: 'dshsc-list' }, group.rows.map(rowView))))
727
- : h('div', { className: 'dshsc-list' }, rows.map(rowView));
728
- return h(
729
- 'div',
730
- { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
731
- updateCount > 0
732
- ? h('div', { className: 'dshsc-banner warn' },
733
- h('span', null, `⚠ ${updateCount} ${t('status.update')}`),
734
- h('button', { className: 'dshsc-btn', disabled: !writable || busy !== null, onClick: () => void updateAll() }, t('action.updateAll')))
735
- : null,
736
- body,
737
- visibleOnlyBanners(),
738
- );
739
- }
740
-
741
- function visibleOnlyBanners() {
742
- const items = data?.visibleOnly || [];
743
- // bundled 是 DSH 自带(archify / office…),不在 canonical 是设计如此,别当成告警
744
- const builtin = items.filter((item) => item.source === 'bundled');
745
- const others = items.filter((item) => item.source !== 'bundled');
746
- return [
747
- others.length > 0
748
- ? h('div', { className: 'dshsc-banner warn', key: 'other' },
749
- `${t('visibleOnlyOther')}:${others.map((item) => item.name).join('、')}`)
750
- : null,
751
- builtin.length > 0
752
- ? h('div', { className: 'dshsc-banner info', key: 'builtin' },
753
- `${t('visibleOnlyBuiltin')}:${builtin.map((item) => item.name).join('、')}`)
754
- : null,
755
- ];
756
- }
757
-
758
- function subscriptionsTab() {
759
- const subs = data?.subscriptions || [];
760
- if (subs.length === 0) {
761
- return h(
762
- 'div',
763
- { className: 'dshsc-empty' },
764
- h('div', { className: 'dshsc-name' }, t('empty.subs')),
765
- h('div', { className: 'dshsc-meta', style: { justifyContent: 'center' } }, t('empty.subsHint')),
766
- h('div', { style: { marginTop: 10 } },
767
- h('button', { className: 'dshsc-btn primary', disabled: !writable, onClick: requestAddSubscription }, t('action.addSubscription'))),
768
- );
769
- }
770
- return h('div', { className: 'dshsc-list' }, subs.map((subscription) => {
771
- const entries = subscription.snapshot?.entries || [];
772
- const installedNames = new Set(rows.filter((row) => row.subscriptionId === subscription.id).map((row) => row.name));
773
- const open = Boolean(expanded[subscription.id]);
774
- return h(
775
- 'div',
776
- { className: 'dshsc-row', key: subscription.id },
777
- h('div', { className: 'dshsc-row-head' },
778
- h('button', { className: 'dshsc-btn ghost', onClick: () => setExpanded((prev) => ({ ...prev, [subscription.id]: !open })) }, open ? '▾' : '▸'),
779
- h('span', { className: 'dshsc-name' }, subscription.source),
780
- h('span', { className: 'dshsc-substat' },
781
- `${subscription.ref || 'default'} · ${entries.length} ${t('skillCount')} · ${t('installed')} ${installedNames.size}`),
782
- h('div', { className: 'dshsc-actions' },
783
- h('button', { className: 'dshsc-btn ghost', disabled: busy !== null, onClick: () => void refreshSubscription(subscription) }, t('action.refresh')),
784
- h('button', { className: 'dshsc-btn ghost danger', disabled: busy !== null, onClick: () => void deleteSubscription(subscription) }, t('action.delete')))),
785
- open
786
- ? h('div', { className: 'dshsc-sub-list' }, entries.map((entry) =>
787
- h('div', { className: 'dshsc-sub-entry', key: entry.name },
788
- h('span', { className: 'dshsc-name' }, entry.name),
789
- h('span', { className: 'dshsc-substat' }, installedNames.has(entry.name) ? t('installed') : t('notInstalled')),
790
- h('div', { className: 'dshsc-actions' },
791
- h('button', {
792
- className: 'dshsc-btn',
793
- disabled: !writable || busy !== null || installedNames.has(entry.name),
794
- onClick: () => void installFromSubscription(subscription, entry),
795
- }, t('action.install'))))))
796
- : null,
797
- );
798
- }));
799
- }
800
-
801
- const autolinkTargets = (data?.agents?.detected || []).filter((agent) => agent.kind === 'agent');
802
- const autoLinkSelection = data?.settings?.autoLinkTargets;
803
- const autoLinkAgents = Array.isArray(autoLinkSelection)
804
- ? autolinkTargets.filter((agent) => autoLinkSelection.includes(agent.target))
805
- : autolinkTargets;
806
- const autoLinkLabel = [t('links.dshGlobal'), ...autoLinkAgents.map((agent) => agent.label)].join('、');
807
- const placement = data?.settings?.placement === 'sidebar' ? 'sidebar' : 'settings';
808
-
809
- async function changePlacement(next) {
810
- if (next === placement || placementBusy) return;
811
- setPlacementBusy(next);
812
- try {
813
- await apiCall('POST', '/settings', { placement: next });
814
- props?.onPlacementChange?.(next);
815
- setPlacementBusy(null);
816
- } catch (err) {
817
- setToast({ kind: 'err', text: err.message });
818
- setPlacementBusy(null);
819
- }
820
- }
821
-
822
- const root = h(
823
- 'div',
824
- { className: 'dshsc-root' },
825
- h('style', null, CSS),
826
- h('div', { className: 'dshsc-head' },
827
- h('div', { className: 'dshsc-heading' },
828
- h(Logo, { size: page ? 36 : 30 }),
829
- h('div', { className: 'dshsc-heading-text' },
830
- h('h2', { className: 'dshsc-title' }, page ? t('page.title') : t('section.title')),
831
- h('p', { className: 'dshsc-sub' }, t('section.subtitle')))),
832
- h('div', { className: 'dshsc-head-actions' },
833
- h('button', {
834
- className: 'dshsc-btn',
835
- disabled: !writable,
836
- title: `${t('autoLink')}:${autoLinkLabel}`,
837
- 'aria-label': `${t('autoLink')}:${autoLinkLabel}`,
838
- onClick: () => setModal({ type: 'autolink' }),
839
- }, `${t('autoLink')} · ${autoLinkAgents.length + 1} ▾`),
840
- h('button', {
841
- className: 'dshsc-btn primary',
842
- disabled: !writable,
843
- onClick: tab === 'installed' ? requestAddSkill : requestAddSubscription,
844
- }, tab === 'installed' ? t('action.addSkill') : t('action.addSubscription')),
845
- h('a', {
846
- className: 'dshsc-btn dshsc-iconbtn',
847
- href: GITHUB_URL,
848
- target: '_blank',
849
- rel: 'noopener noreferrer',
850
- title: t('github.open'),
851
- 'aria-label': t('github.open'),
852
- }, h(GitHubIcon, { size: 16 })))),
853
-
854
- data ? h('div', { className: 'dshsc-prefbar' },
855
- h('span', null, t('placement.label')),
856
- h('div', { className: 'dshsc-segment' },
857
- h('button', {
858
- className: 'dshsc-btn',
859
- 'aria-pressed': String(placement === 'settings'),
860
- disabled: Boolean(placementBusy) || Boolean(data.readOnly),
861
- onClick: () => void changePlacement('settings'),
862
- }, t('placement.settings')),
863
- h('button', {
864
- className: 'dshsc-btn',
865
- 'aria-pressed': String(placement === 'sidebar'),
866
- disabled: Boolean(placementBusy) || Boolean(data.readOnly),
867
- onClick: () => void changePlacement('sidebar'),
868
- }, t('placement.sidebar'))),
869
- h('span', { className: 'dshsc-pref-hint' }, t('placement.hint'))) : null,
870
-
871
- error ? h('div', { className: 'dshsc-banner err' }, `${error} · `, h('button', { className: 'dshsc-btn ghost', onClick: () => void reload() }, t('action.retry'))) : null,
872
- data?.readOnly ? h('div', { className: 'dshsc-banner warn' }, t('banner.readonly')) : null,
873
- data && !data.cli?.usable ? h('div', { className: 'dshsc-banner err' }, `${t('banner.cli')} · ${data.cli?.source || ''} ${data.cli?.version || ''}`) : null,
874
-
875
- h('div', { className: 'dshsc-toolbar' },
876
- h('div', { className: 'dshsc-tabs' },
877
- h('button', { className: `dshsc-tab${tab === 'installed' ? ' active' : ''}`, onClick: () => setTab('installed') },
878
- `${t('tab.installed')}(${rows.length})`),
879
- h('button', { className: `dshsc-tab${tab === 'subscriptions' ? ' active' : ''}`, onClick: () => setTab('subscriptions') },
880
- `${t('tab.subscriptions')}(${data?.subscriptions?.length || 0})`)),
881
- h('div', { className: 'dshsc-spacer' }),
882
- tab === 'installed' ? [
883
- h('button', { key: 'refresh', className: 'dshsc-btn', disabled: busy !== null, onClick: () => void checkUpdates() }, t('action.refresh')),
884
- h('div', { key: 'view', className: 'dshsc-segment' },
885
- h('button', { className: 'dshsc-btn', 'aria-pressed': String(view === 'flat'), onClick: () => setView('flat') }, t('view.flat')),
886
- h('button', { className: 'dshsc-btn', 'aria-pressed': String(view === 'agent'), onClick: () => setView('agent') }, t('view.agent'))),
887
- ] : null),
888
-
889
- tab === 'installed' ? installedTab() : subscriptionsTab(),
890
-
891
- busy ? h('div', { className: 'dshsc-meta' }, `${t('busy.working')} ${busy}`) : null,
892
- toast ? h('div', { className: `dshsc-toast ${toast.kind}` }, toast.text) : null,
893
- modal ? renderModal() : null,
894
- );
895
- return page ? h('div', { className: 'dshsc-page' }, root) : root;
896
-
897
- function renderModal() {
898
- if (modal.type === 'add-skill') {
899
- return h(FormModal, {
900
- title: t('action.addSkill'),
901
- hint: t('form.hint'),
902
- fields: [
903
- { key: 'source', label: t('form.source'), placeholder: t('form.sourceHint') },
904
- { key: 'skills', label: t('form.skills'), placeholder: 'git-kit, handoff' },
905
- ],
906
- onCancel: () => setModal(null),
907
- onSubmit: (form) => void addSkill(form),
908
- });
909
- }
910
- if (modal.type === 'add-subscription') {
911
- return h(FormModal, {
912
- title: t('action.addSubscription'),
913
- hint: t('form.subHint'),
914
- fields: [
915
- { key: 'source', label: t('form.source'), placeholder: t('form.sourceHint') },
916
- { key: 'ref', label: t('form.ref'), placeholder: 'main' },
917
- { key: 'subpath', label: t('form.subpath'), placeholder: 'skills' },
918
- ],
919
- onCancel: () => setModal(null),
920
- onSubmit: (form) => void addSubscription(form),
921
- });
922
- }
923
- if (modal.type === 'links') {
924
- return h(LinksModal, {
925
- row: modal.row,
926
- agents: data?.agents?.all || [],
927
- workspaces: data?.workspaces || [],
928
- onCancel: () => setModal(null),
929
- onSave: (targets, scopeProjects) => {
930
- setModal(null);
931
- void saveLinks(modal.row, targets, scopeProjects);
932
- },
933
- });
934
- }
935
- if (modal.type === 'autolink') {
936
- return h(AutoLinkModal, {
937
- settings: data?.settings || {},
938
- agents: autolinkTargets,
939
- onCancel: () => setModal(null),
940
- onSave: async (targets) => {
941
- setModal(null);
942
- await run('settings', () => apiCall('POST', '/settings', { autoLinkTargets: targets }));
943
- },
944
- });
945
- }
946
- if (modal.type === 'confirm-delete') {
947
- return h(ConfirmModal, {
948
- title: t('confirm.deleteTitle'),
949
- body: t('confirm.deleteBody'),
950
- danger: true,
951
- onCancel: () => setModal(null),
952
- onConfirm: modal.onConfirm,
953
- });
954
- }
955
- if (modal.type === 'confirm-update') {
956
- return h(ConfirmModal, {
957
- title: t('confirm.updateTitle'),
958
- body: t('confirm.updateBody'),
959
- onCancel: () => setModal(null),
960
- onConfirm: modal.onConfirm,
961
- });
962
- }
963
- if (modal.type === 'conflict') {
964
- const conflicts = modal.detail?.conflicts || [];
965
- return h(ConfirmModal, {
966
- title: t('error.conflictTitle'),
967
- body: `${modal.message}\n${conflicts.map((item) => `${item.label}: ${item.linkPath}`).join('\n')}`,
968
- onCancel: () => setModal(null),
969
- onConfirm: () => setModal(null),
970
- confirmLabel: t('action.cancel'),
971
- });
972
- }
973
- return null;
974
- }
975
- }
976
-
977
- function Overlay({ title, children, footer, onCancel }) {
978
- React.useEffect(() => {
979
- const onKey = (event) => {
980
- if (event.key === 'Escape') onCancel?.();
981
- };
982
- window.addEventListener('keydown', onKey);
983
- return () => window.removeEventListener('keydown', onKey);
984
- }, [onCancel]);
985
- return h('div', { className: 'dshsc-overlay', onClick: (event) => { if (event.target === event.currentTarget) onCancel?.(); } },
986
- h('div', { className: 'dshsc-modal', role: 'dialog', 'aria-modal': 'true' },
987
- h('button', {
988
- className: 'dshsc-modal-close',
989
- 'aria-label': t('action.cancel'),
990
- onClick: (event) => { event.stopPropagation(); onCancel?.(); },
991
- }, '×'),
992
- h('h3', null, title),
993
- children,
994
- h('div', { className: 'dshsc-modal-foot' }, footer)));
995
- }
996
-
997
- function FormModal({ title, hint, fields, onCancel, onSubmit }) {
998
- const [form, setForm] = React.useState(() => Object.fromEntries(fields.map((field) => [field.key, ''])));
999
- return h(Overlay, {
1000
- title,
1001
- onCancel,
1002
- footer: [
1003
- h('button', { key: 'c', className: 'dshsc-btn ghost', onClick: onCancel }, t('action.cancel')),
1004
- h('button', {
1005
- key: 's',
1006
- className: 'dshsc-btn primary',
1007
- disabled: !form[fields[0].key]?.trim?.() || (fields.some((f) => f.key === 'skills') && !form.skills?.trim()),
1008
- onClick: () => onSubmit(form),
1009
- }, t('action.save')),
1010
- ],
1011
- },
1012
- hint ? h('p', { className: 'dshsc-sub' }, hint) : null,
1013
- ...fields.map((field) =>
1014
- h('label', { className: 'dshsc-field', key: field.key },
1015
- h('span', null, field.label),
1016
- h('input', {
1017
- value: form[field.key],
1018
- placeholder: field.placeholder,
1019
- onChange: (event) => setForm((prev) => ({ ...prev, [field.key]: event.target.value })),
1020
- }))));
1021
- }
1022
-
1023
- function ConfirmModal({ title, body, onCancel, onConfirm, danger, confirmLabel }) {
1024
- return h(Overlay, {
1025
- title,
1026
- onCancel,
1027
- footer: [
1028
- h('button', { key: 'c', className: 'dshsc-btn ghost', onClick: onCancel }, t('action.cancel')),
1029
- h('button', { key: 'o', className: `dshsc-btn${danger ? ' danger' : ' primary'}`, onClick: onConfirm }, confirmLabel || t('action.save')),
1030
- ],
1031
- }, h('p', { className: 'dshsc-sub', style: { whiteSpace: 'pre-wrap' } }, body));
1032
- }
1033
-
1034
- function LinksModal({ row, agents, workspaces, onCancel, onSave }) {
1035
- // 本机没有目录的 Agent 不展示;dsh(全局)必选,永远显示
1036
- const candidates = agents.filter(
1037
- (agent) => agent.kind === 'dsh' || (agent.kind !== 'project' && agent.exists),
1038
- );
1039
- const [targets, setTargets] = React.useState(() => {
1040
- // 只保留还能展示的目标:agent 目录都没了就别在保存时把它重新建出来
1041
- const visible = new Set(candidates.map((agent) => agent.target));
1042
- const initial = new Set(row.links.map((link) => link.target).filter((target) => visible.has(target)));
1043
- initial.add('dsh-global');
1044
- return initial;
1045
- });
1046
- const [scopeProjects, setScopeProjects] = React.useState(row.scopeProjects || []);
1047
- const [scopeMode, setScopeMode] = React.useState((row.scopeProjects || []).length > 0 ? 'projects' : 'global');
1048
- const workspacePaths = new Set(workspaces.map((workspace) => workspace.path));
1049
- const otherProjects = scopeProjects.filter((project) => !workspacePaths.has(project));
1050
- const toggle = (target) => setTargets((prev) => {
1051
- const next = new Set(prev);
1052
- if (next.has(target)) next.delete(target);
1053
- else next.add(target);
1054
- return next;
1055
- });
1056
- const toggleProject = (project) => setScopeProjects((prev) =>
1057
- prev.includes(project) ? prev.filter((item) => item !== project) : [...prev, project]);
1058
- const pickDirectory = async () => {
1059
- const picker = ctx.get('uiWorkspace');
1060
- const dir = await picker?.pickDirectory?.();
1061
- if (!dir) return;
1062
- setScopeProjects((prev) => (prev.includes(dir) ? prev : [...prev, dir]));
1063
- };
1064
- return h(Overlay, {
1065
- title: `${t('action.links')} · ${row.name}`,
1066
- onCancel,
1067
- footer: [
1068
- h('button', { key: 'c', className: 'dshsc-btn ghost', onClick: onCancel }, t('action.cancel')),
1069
- h('button', {
1070
- key: 's',
1071
- className: 'dshsc-btn primary',
1072
- onClick: () => onSave([...targets], scopeMode === 'projects' ? scopeProjects.filter((item) => item.trim()) : []),
1073
- }, t('action.save')),
1074
- ],
1075
- },
1076
- h('div', { className: 'dshsc-group' }, t('links.targets')),
1077
- h('p', { className: 'dshsc-sub' }, t('links.detectedHint')),
1078
- ...candidates.map((agent) =>
1079
- h('label', { className: 'dshsc-check', key: agent.target },
1080
- h('input', {
1081
- type: 'checkbox',
1082
- checked: agent.target === 'dsh-global' ? true : targets.has(agent.target),
1083
- disabled: agent.target === 'dsh-global',
1084
- onChange: () => toggle(agent.target),
1085
- }),
1086
- h('span', null, agent.kind === 'dsh' ? t('links.dshGlobal') : agent.label),
1087
- agent.target === 'dsh-global' ? h('small', null, t('links.required')) : null,
1088
- h('small', null, agent.dir))),
1089
- h('div', { className: 'dshsc-group' }, t('links.scope')),
1090
- h('label', { className: 'dshsc-check' },
1091
- h('input', { type: 'radio', name: `scope-${row.name}`, checked: scopeMode === 'global', onChange: () => setScopeMode('global') }),
1092
- h('span', null, t('links.scopeGlobal'))),
1093
- h('label', { className: 'dshsc-check' },
1094
- h('input', { type: 'radio', name: `scope-${row.name}`, checked: scopeMode === 'projects', onChange: () => setScopeMode('projects') }),
1095
- h('span', null, t('links.scopeProjects'))),
1096
- scopeMode === 'projects'
1097
- ? h('div', null,
1098
- h('div', { className: 'dshsc-group' }, t('links.workspaces')),
1099
- workspaces.length === 0
1100
- ? h('p', { className: 'dshsc-sub' }, t('links.noWorkspaces'))
1101
- : h('div', { className: 'dshsc-panel' }, workspaces.map((workspace) =>
1102
- h('label', { className: 'dshsc-check', key: workspace.id },
1103
- h('input', {
1104
- type: 'checkbox',
1105
- checked: scopeProjects.includes(workspace.path),
1106
- onChange: () => toggleProject(workspace.path),
1107
- }),
1108
- h('span', null, workspace.title),
1109
- h('small', null, workspace.path)))),
1110
- otherProjects.length > 0
1111
- ? h('div', { className: 'dshsc-group' }, t('links.selected'))
1112
- : null,
1113
- ...otherProjects.map((project) =>
1114
- h('div', { className: 'dshsc-sub-entry', key: project },
1115
- h('span', { className: 'dshsc-code' }, project),
1116
- h('div', { className: 'dshsc-actions' },
1117
- h('button', {
1118
- className: 'dshsc-btn ghost danger',
1119
- onClick: () => setScopeProjects((prev) => prev.filter((item) => item !== project)),
1120
- }, t('action.delete'))))),
1121
- h('button', { className: 'dshsc-btn', style: { alignSelf: 'flex-start', marginTop: 6 }, onClick: () => void pickDirectory() }, t('links.pickDirectory')))
1122
- : null,
1123
- h('p', { className: 'dshsc-sub' }, t('links.hint')));
1124
- }
1125
-
1126
- function AutoLinkModal({ settings, agents, onCancel, onSave }) {
1127
- const [targets, setTargets] = React.useState(() => new Set(
1128
- Array.isArray(settings.autoLinkTargets) ? settings.autoLinkTargets : agents.map((agent) => agent.target),
1129
- ));
1130
- const toggle = (target) => setTargets((prev) => {
1131
- const next = new Set(prev);
1132
- if (next.has(target)) next.delete(target);
1133
- else next.add(target);
1134
- return next;
1135
- });
1136
- return h(Overlay, {
1137
- title: t('autoLink'),
1138
- onCancel,
1139
- footer: [
1140
- h('button', { key: 'c', className: 'dshsc-btn ghost', onClick: onCancel }, t('action.cancel')),
1141
- h('button', { key: 's', className: 'dshsc-btn primary', onClick: () => onSave([...targets]) }, t('action.save')),
1142
- ],
1143
- },
1144
- h('p', { className: 'dshsc-sub' }, t('links.dshGlobal') + ' · ' + t('links.required')),
1145
- h('div', { className: 'dshsc-group' }, t('autoLink.detected')),
1146
- agents.length === 0
1147
- ? h('p', { className: 'dshsc-sub' }, t('autoLink.none'))
1148
- : h('div', { className: 'dshsc-panel' }, agents.map((agent) =>
1149
- h('label', { className: 'dshsc-check', key: agent.target },
1150
- h('input', {
1151
- type: 'checkbox',
1152
- checked: targets.has(agent.target),
1153
- onChange: () => toggle(agent.target),
1154
- }),
1155
- h('span', null, agent.label),
1156
- h('small', null, agent.dir)))),
1157
- h('p', { className: 'dshsc-sub' }, t('autoLink.hint')));
1158
- }
1159
-
1160
- const PANEL_ID = 'dsh-skills-console';
1161
- const layout = ctx.get('layout');
1162
- let surface = null;
1163
- let userSelected = false;
1164
- const surfaceDisposers = [];
1165
-
1166
- function SettingsSurface(props) {
1167
- return h(Section, {
1168
- ...props,
1169
- page: false,
1170
- onPlacementChange: (next) => changeSurface(next, props?.close),
1171
- });
1172
- }
1173
-
1174
- function PageSurface() {
1175
- return h(Section, { page: true, onPlacementChange: (next) => changeSurface(next) });
1176
- }
1177
-
1178
- function SurfaceIcon(props) {
1179
- return h(PanelIcon, { size: props?.size || 16 });
1180
- }
1181
-
1182
- function clearSurface() {
1183
- while (surfaceDisposers.length > 0) {
1184
- const dispose = surfaceDisposers.pop();
1185
- try {
1186
- dispose();
1187
- } catch {
1188
- // 单个卸载失败不阻塞另一个入口
1189
- }
1190
- }
1191
- }
1192
-
1193
- function mount(next) {
1194
- const target = next === 'sidebar' ? 'sidebar' : 'settings';
1195
- if (surface === target) return;
1196
- clearSurface();
1197
- surface = target;
1198
- if (target === 'sidebar') {
1199
- // 先注册 main 再注册侧栏按钮:按钮出现时 main 一定可选
1200
- surfaceDisposers.push(slots.inject('main', () => {
1201
- const disposeMain = slots.register({ name: 'main', key: PANEL_ID }, PageSurface);
1202
- const disposeIcon = slots.inject('sidebar.panellist', () => slots.register({
1203
- name: 'sidebar.panellist', id: PANEL_ID, order: 30, label: () => t('page.title'),
1204
- }, SurfaceIcon));
1205
- return [disposeMain, disposeIcon];
1206
- }));
1207
- } else {
1208
- surfaceDisposers.push(slots.inject('settings.section', () =>
1209
- slots.register({
1210
- name: 'settings.section', id: PANEL_ID, order: 26, label: () => t('section.title'),
1211
- }, SettingsSurface)));
1212
- }
1213
- }
1214
-
1215
- function changeSurface(next, close) {
1216
- userSelected = true;
1217
- mount(next);
1218
- try {
1219
- if (next === 'sidebar') {
1220
- layout?.selectPanel?.(PANEL_ID);
1221
- close?.();
1222
- } else {
1223
- // 离开已消失的自定义主面板,回到会话;用户从侧栏底部打开设置即可看到分区
1224
- layout?.selectPanel?.('conversation');
1225
- }
1226
- } catch {
1227
- // main 声明尚未就绪时 select 会抛错;侧栏按钮/设置分区随后仍可点开
1228
- }
1229
- }
1230
-
1231
- mount('settings');
1232
- ctx.effect(() => () => {
1233
- clearSurface();
1234
- surface = null;
1235
- }, 'dsh-skills-console:surface');
1236
-
1237
- // 读取入口偏好;晚到的响应不能覆盖用户刚才在页面里的选择
1238
- apiCall('GET', '/state')
1239
- .then((state) => {
1240
- if (!userSelected && state?.settings?.placement === 'sidebar') mount('sidebar');
1241
- })
1242
- .catch(() => {});
1243
- },
1244
- };
2335
+ }
2336
+ function changeSurface(next, close) {
2337
+ userSelected = true;
2338
+ mount(next);
2339
+ try {
2340
+ if (next === "sidebar") {
2341
+ layout?.selectPanel?.(PANEL_ID);
2342
+ close?.();
2343
+ } else {
2344
+ layout?.selectPanel?.("conversation");
2345
+ }
2346
+ } catch {
2347
+ }
2348
+ }
2349
+ function dispose() {
2350
+ clearSurface();
2351
+ surface = null;
2352
+ }
2353
+ return { mount, changeSurface, clearSurface, dispose, hasUserSelected: () => userSelected };
2354
+ }
2355
+
2356
+ // client/app.js
2357
+ var inject = ["slots"];
2358
+ function apply(ctx) {
2359
+ const locale = ctx.get("locale");
2360
+ const t = buildTranslate(locale);
2361
+ const slots = ctx.get("slots");
2362
+ if (!slots?.register) return;
2363
+ installSettingsNavIcon(ctx, () => t("section.title"));
2364
+ const pickDirectory = async () => ctx.get("uiWorkspace")?.pickDirectory?.();
2365
+ const surface = createSurface({ slots, layout: ctx.get("layout"), t, pickDirectory });
2366
+ surface.mount("settings");
2367
+ ctx.effect(() => () => {
2368
+ surface.dispose();
2369
+ }, "dsh-skills-console:surface");
2370
+ apiCall("GET", "/state").then((state) => {
2371
+ if (!surface.hasUserSelected() && state?.settings?.placement === "sidebar") surface.mount("sidebar");
2372
+ }).catch(() => {
2373
+ });
2374
+ }
2375
+ return module.exports;
1245
2376
  },
1246
2377
  });
2378
+