@morehao/dsh-skills-console 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/client.js +1246 -0
- package/cordis.patch.yml +8 -0
- package/icon.svg +15 -0
- package/lib/agent-table.json +331 -0
- package/lib/core/agents.js +124 -0
- package/lib/core/cli.js +232 -0
- package/lib/core/hash.js +46 -0
- package/lib/core/inventory.js +174 -0
- package/lib/core/links.js +104 -0
- package/lib/core/log.js +23 -0
- package/lib/core/paths.js +54 -0
- package/lib/core/service.js +897 -0
- package/lib/core/state.js +149 -0
- package/lib/core/trash.js +142 -0
- package/lib/core/updates.js +138 -0
- package/lib/index.js +77 -0
- package/lib/routes.js +163 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +68 -1
package/client.js
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client half:设置页「技能控制台」分区。
|
|
3
|
+
* 一页两 tab:已装 / 订阅。行内操作列 = 开关 + 更新 + 接入设置 + 删除。
|
|
4
|
+
* 所有数据来自 Host 的 /api/morehao/skills-console/*,这里只做展示与提交。
|
|
5
|
+
*/
|
|
6
|
+
window.__ModuleLoader__.load({
|
|
7
|
+
id: '@morehao/dsh-skills-console',
|
|
8
|
+
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) : '';
|
|
324
|
+
}
|
|
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',
|
|
332
|
+
});
|
|
333
|
+
let json = {};
|
|
334
|
+
try {
|
|
335
|
+
json = await response.json();
|
|
336
|
+
} catch {
|
|
337
|
+
json = {};
|
|
338
|
+
}
|
|
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
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildTranslate(locale) {
|
|
349
|
+
const active = () => {
|
|
350
|
+
try {
|
|
351
|
+
return locale?.getLocale?.().active || 'zh';
|
|
352
|
+
} catch {
|
|
353
|
+
return 'zh';
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
return (key) => {
|
|
357
|
+
const id = active();
|
|
358
|
+
const pack = DICT[id] || DICT.zh;
|
|
359
|
+
return pack[key] ?? DICT.zh[key] ?? key;
|
|
360
|
+
};
|
|
361
|
+
}
|
|
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',
|
|
377
|
+
}),
|
|
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
|
+
}));
|
|
386
|
+
}
|
|
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' },
|
|
393
|
+
},
|
|
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' }));
|
|
400
|
+
}
|
|
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',
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
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
|
+
};
|
|
1245
|
+
},
|
|
1246
|
+
});
|