@restregular/dsh-powerup 0.0.0-stage → 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 +95 -2
- package/client.js +2413 -0
- package/cordis.patch.yml +7 -0
- package/icon.svg +22 -0
- package/index.js +119 -0
- package/lib/restart-helper.cjs +196 -0
- package/lib/restart.js +136 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +70 -4
package/client.js
ADDED
|
@@ -0,0 +1,2413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-powerup — Client half.
|
|
3
|
+
*
|
|
4
|
+
* Power-Up is the home for small, opt-in upgrades to the Harness UI. The first
|
|
5
|
+
* upgrade is a context menu in the built-in Trajectory ledger:
|
|
6
|
+
*
|
|
7
|
+
* - right-click a record row to copy its text, copy a Markdown quote, quote
|
|
8
|
+
* it into the composer, open its inspector, fork the session at that
|
|
9
|
+
* record, or copy the session id;
|
|
10
|
+
* - right-click the empty ledger area for session-level actions (fork the
|
|
11
|
+
* latest completed turn, export the trajectory snapshot, open this page).
|
|
12
|
+
*
|
|
13
|
+
* Implementation notes:
|
|
14
|
+
* - The ledger belongs to `@deepseek-ai/dsh-client-ui-trajectory`. We do not
|
|
15
|
+
* import its modules; we observe its own `data-*` contracts and read the
|
|
16
|
+
* session-scoped `useTrajectory` hook from a tiny header utility.
|
|
17
|
+
* - `shell.overlay` owns the menu and the toast. It is root-scoped, so a
|
|
18
|
+
* hidden `conversation.session.header.utilities` bridge publishes the
|
|
19
|
+
* active session id, trajectory snapshot and composer actions into a
|
|
20
|
+
* module-level store.
|
|
21
|
+
* - Every surface uses `--dsw-alias-*` theme tokens, so light and dark themes
|
|
22
|
+
* are inherited from the host. Literal colors exist only inside the brand
|
|
23
|
+
* SVG artwork.
|
|
24
|
+
* - The whole file is wrapped in an IIFE: client bundles are concatenated
|
|
25
|
+
* into one classic-script realm and top-level bindings would collide.
|
|
26
|
+
*/
|
|
27
|
+
(() => {
|
|
28
|
+
const MODULE_ID = '@restregular/dsh-powerup';
|
|
29
|
+
|
|
30
|
+
/** Whether the host facade already holds a factory for this module id. */
|
|
31
|
+
function alreadyRegistered() {
|
|
32
|
+
const factories = window.__ModuleLoader__?.factories;
|
|
33
|
+
return factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (alreadyRegistered()) return;
|
|
37
|
+
|
|
38
|
+
window.__ModuleLoader__.load({
|
|
39
|
+
id: MODULE_ID,
|
|
40
|
+
factory: (require) => {
|
|
41
|
+
const React = require('react');
|
|
42
|
+
const h = React.createElement;
|
|
43
|
+
const { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } = React;
|
|
44
|
+
|
|
45
|
+
const NS = 'dsh-powerup';
|
|
46
|
+
const SETTINGS_KEY = 'dsh-powerup:settings:v1';
|
|
47
|
+
const VERSION = '0.2.0';
|
|
48
|
+
const MENU_GAP = 8;
|
|
49
|
+
const MENU_WIDTH = 248;
|
|
50
|
+
|
|
51
|
+
// ------------------------------------------------------------------
|
|
52
|
+
// Dictionaries
|
|
53
|
+
// ------------------------------------------------------------------
|
|
54
|
+
const DICT = {
|
|
55
|
+
zh: {
|
|
56
|
+
'panel.title': 'Power-Up',
|
|
57
|
+
'page.title': 'Power-Up',
|
|
58
|
+
'page.subtitle': '给 DeepSeek Harness 加一点助推。',
|
|
59
|
+
'page.featureTitle': '轨迹右键菜单',
|
|
60
|
+
'page.featureDesc': '在轨迹页面右键任意记录,快速复制、引用、查看详情,或从该记录 Fork 出一个新会话。',
|
|
61
|
+
'page.enabled': '已启用',
|
|
62
|
+
'page.disabled': '已停用',
|
|
63
|
+
'page.actions': '可用操作',
|
|
64
|
+
'page.howTitle': '怎么用',
|
|
65
|
+
'page.how1': '打开任意会话,切到顶部的「轨迹」视图。',
|
|
66
|
+
'page.how2': '在记录行上右键;在空白处右键可执行会话级操作。',
|
|
67
|
+
'page.how3': '菜单支持 ↑ / ↓ 选择、Enter 执行、Esc 关闭。',
|
|
68
|
+
'page.updateTitle': '插件自更新',
|
|
69
|
+
'page.updateDesc': '在插件页为每个第三方已安装插件提供更新入口:从安装源选版本、卸载旧版、按「添加插件」流程重装,并可立即启用与重启 DSH。',
|
|
70
|
+
'page.updateHow': '打开侧边栏的「插件」页,在「已安装插件」分组中点击卡片右侧的更新图标。',
|
|
71
|
+
'page.footer': 'Power-Up {version} · 设置在浏览器本地保存',
|
|
72
|
+
'page.localOnly': '仅影响当前浏览器',
|
|
73
|
+
'action.copy': '复制内容',
|
|
74
|
+
'action.copyHint': '复制这一条记录的完整文本',
|
|
75
|
+
'action.quote': '复制为引用',
|
|
76
|
+
'action.quoteHint': '生成可直接粘贴的 Markdown 引用',
|
|
77
|
+
'action.insert': '在输入框引用',
|
|
78
|
+
'action.insertHint': '把引用插入当前会话的输入框',
|
|
79
|
+
'action.inspect': '查看记录详情',
|
|
80
|
+
'action.inspectHint': '打开宿主自带的记录检查器',
|
|
81
|
+
'action.fork': '从此处 Fork 会话',
|
|
82
|
+
'action.forkHint': '带着这条记录及其之前的内容分支',
|
|
83
|
+
'action.export': '导出轨迹 JSON',
|
|
84
|
+
'action.exportHint': '把当前轨迹快照保存为 JSON 文件',
|
|
85
|
+
'action.session': '复制会话 ID',
|
|
86
|
+
'action.sessionHint': '方便在日志、issue 里引用',
|
|
87
|
+
'action.panel': '打开 Power-Up 页面',
|
|
88
|
+
'action.panelHint': '回到这个功能控制台',
|
|
89
|
+
'menu.title': 'Power-Up',
|
|
90
|
+
'menu.record': '轨迹记录',
|
|
91
|
+
'menu.copy': '复制内容',
|
|
92
|
+
'menu.quote': '复制为引用',
|
|
93
|
+
'menu.insert': '在输入框引用',
|
|
94
|
+
'menu.inspect': '查看记录详情',
|
|
95
|
+
'menu.fork': '从此处 Fork 会话',
|
|
96
|
+
'menu.forkHint': '含此条',
|
|
97
|
+
'menu.export': '导出轨迹 JSON',
|
|
98
|
+
'menu.session': '复制会话 ID',
|
|
99
|
+
'menu.panel': '打开 Power-Up 页面',
|
|
100
|
+
'menu.noFork': '这条记录没有可用的 Fork 位置',
|
|
101
|
+
'menu.blank': '会话操作',
|
|
102
|
+
'toast.copied': '已复制',
|
|
103
|
+
'toast.copyFailed': '复制失败',
|
|
104
|
+
'toast.inserted': '已插入输入框',
|
|
105
|
+
'toast.insertFallback': '输入框不可用,已复制到剪贴板',
|
|
106
|
+
'toast.forked': '已 Fork 会话',
|
|
107
|
+
'toast.forkFailed': 'Fork 失败:{message}',
|
|
108
|
+
'toast.exported': '轨迹已导出',
|
|
109
|
+
'toast.exportFailed': '导出失败:{message}',
|
|
110
|
+
'toast.empty': '这条记录没有可复制的文本',
|
|
111
|
+
'toast.actionFailed': '操作失败:{message}',
|
|
112
|
+
'toast.sessionCopied': '会话 ID 已复制',
|
|
113
|
+
'toast.forkUnavailable': '当前宿主没有提供 Fork 接口',
|
|
114
|
+
'toast.noSession': '还没有打开会话',
|
|
115
|
+
'update.button': '更新版本',
|
|
116
|
+
'update.title': '更新插件',
|
|
117
|
+
'update.subtitle': '选择版本后自动卸载旧版并安装新版本。',
|
|
118
|
+
'update.current': '当前版本',
|
|
119
|
+
'update.versionLabel': '版本号',
|
|
120
|
+
'update.versionPlaceholder': '输入版本号,或从列表选择',
|
|
121
|
+
'update.loadingVersions': '正在从安装源读取版本…',
|
|
122
|
+
'update.loading': '正在读取插件信息…',
|
|
123
|
+
'update.chooseVersion': '选择版本',
|
|
124
|
+
'update.versionsFailed': '读取版本失败:{message}',
|
|
125
|
+
'update.versionsEmpty': '安装源没有返回可用版本',
|
|
126
|
+
'update.latest': '最新',
|
|
127
|
+
'update.retry': '重试',
|
|
128
|
+
'update.registry': '安装源',
|
|
129
|
+
'update.registryDefault': '默认安装源',
|
|
130
|
+
'update.registryMirror': '中国大陆镜像源',
|
|
131
|
+
'update.registryCustom': '自定义地址',
|
|
132
|
+
'update.registryFrom': '版本列表来自 {registry}',
|
|
133
|
+
'update.start': '开始更新',
|
|
134
|
+
'update.cancel': '取消',
|
|
135
|
+
'update.close': '关闭',
|
|
136
|
+
'update.step.checking': '正在校验版本…',
|
|
137
|
+
'update.step.removing': '正在卸载旧版…',
|
|
138
|
+
'update.step.installing': '正在安装 {version}…',
|
|
139
|
+
'update.step.enabling': '正在启用…',
|
|
140
|
+
'update.log': '安装输出',
|
|
141
|
+
'update.enableTitle': '安装完成,是否立即启用?',
|
|
142
|
+
'update.enableDesc': '启用后新版本才会加载。',
|
|
143
|
+
'update.enableNow': '立即启用',
|
|
144
|
+
'update.enableLater': '稍后启用',
|
|
145
|
+
'update.enableFailed': '启用失败:{message}',
|
|
146
|
+
'update.restartTitle': '是否立即重启 DSH?',
|
|
147
|
+
'update.restartDesc': '重启会关闭并重新打开 DSH,正在运行的任务会中断。',
|
|
148
|
+
'update.restartNow': '立即重启',
|
|
149
|
+
'update.restartLater': '稍后重启',
|
|
150
|
+
'update.restartUnsupported': '当前运行方式无法自动重启:{reason}',
|
|
151
|
+
'update.restarting': 'DSH 正在重启,窗口即将关闭…',
|
|
152
|
+
'update.restartFailed': '重启请求失败:{message}',
|
|
153
|
+
'update.doneTitle': '更新完成',
|
|
154
|
+
'update.doneInstalled': '{name} 已更新到 {version}。',
|
|
155
|
+
'update.doneDisabled': '{name} 已安装 {version},尚未启用。',
|
|
156
|
+
'update.doneEnabled': '新版本已启用。',
|
|
157
|
+
'update.doneRestart': '部分更改需要重启 DSH 后生效。',
|
|
158
|
+
'update.doneButton': '完成',
|
|
159
|
+
'update.errorTitle': '更新失败',
|
|
160
|
+
'update.selfTitle': '请用「添加插件」更新 Power-Up 自身',
|
|
161
|
+
'update.selfDesc': '更新 Power-Up 会先卸载本插件,更新流程会随之中断。',
|
|
162
|
+
'update.notRemovable': '该插件受 DSH 管理,不能在这里更新。',
|
|
163
|
+
'update.notBundle': '该包不是 DSH 插件包。',
|
|
164
|
+
'update.alreadyInstalled': '该包已安装,请先卸载旧版本。',
|
|
165
|
+
'update.notFound': '安装源里找不到这个版本。',
|
|
166
|
+
'update.cancelled': '更新已取消。',
|
|
167
|
+
'update.installedChip': '未安装',
|
|
168
|
+
'update.unknown': '未知错误',
|
|
169
|
+
'update.invalidVersion': '请输入有效版本号(例如 1.2.3)',
|
|
170
|
+
'update.stopProfile': '当前 profile 不能热卸载该插件,请重启 DSH 后再试。',
|
|
171
|
+
'update.unsupportedTitle': '无法自动更新',
|
|
172
|
+
},
|
|
173
|
+
en: {
|
|
174
|
+
'panel.title': 'Power-Up',
|
|
175
|
+
'page.title': 'Power-Up',
|
|
176
|
+
'page.subtitle': 'A little boost for DeepSeek Harness.',
|
|
177
|
+
'page.featureTitle': 'Trajectory context menu',
|
|
178
|
+
'page.featureDesc': 'Right-click any trajectory record to copy it, quote it, open its inspector, or fork the session at that point.',
|
|
179
|
+
'page.enabled': 'Enabled',
|
|
180
|
+
'page.disabled': 'Disabled',
|
|
181
|
+
'page.actions': 'What you can do',
|
|
182
|
+
'page.howTitle': 'How to use it',
|
|
183
|
+
'page.how1': 'Open a session and switch to the Trajectory view.',
|
|
184
|
+
'page.how2': 'Right-click a record row; right-click empty space for session actions.',
|
|
185
|
+
'page.how3': 'Use ↑ / ↓ to move, Enter to run, Esc to close.',
|
|
186
|
+
'page.updateTitle': 'Update plugins',
|
|
187
|
+
'page.updateDesc': 'Every third-party plugin on the Plugins page gains an update entry: pick a version from a registry, remove the old copy, reinstall through the Add plugin path, then enable and restart DSH.',
|
|
188
|
+
'page.updateHow': 'Open Plugins in the sidebar and click the update icon at the right of a card under Installed plugins.',
|
|
189
|
+
'page.footer': 'Power-Up {version} · Settings are stored in this browser',
|
|
190
|
+
'page.localOnly': 'This browser only',
|
|
191
|
+
'action.copy': 'Copy content',
|
|
192
|
+
'action.copyHint': 'Copy the full text of this record',
|
|
193
|
+
'action.quote': 'Copy as quote',
|
|
194
|
+
'action.quoteHint': 'Produce a Markdown quote ready to paste',
|
|
195
|
+
'action.insert': 'Quote in composer',
|
|
196
|
+
'action.insertHint': 'Insert the quote into the current composer',
|
|
197
|
+
'action.inspect': 'Open record details',
|
|
198
|
+
'action.inspectHint': 'Open the host record inspector',
|
|
199
|
+
'action.fork': 'Fork from here',
|
|
200
|
+
'action.forkHint': 'Branch with this record and everything before it',
|
|
201
|
+
'action.export': 'Export trajectory JSON',
|
|
202
|
+
'action.exportHint': 'Save the current trajectory snapshot as JSON',
|
|
203
|
+
'action.session': 'Copy session ID',
|
|
204
|
+
'action.sessionHint': 'Useful in logs and issues',
|
|
205
|
+
'action.panel': 'Open the Power-Up page',
|
|
206
|
+
'action.panelHint': 'Return to this control panel',
|
|
207
|
+
'menu.title': 'Power-Up',
|
|
208
|
+
'menu.record': 'Trajectory record',
|
|
209
|
+
'menu.copy': 'Copy content',
|
|
210
|
+
'menu.quote': 'Copy as quote',
|
|
211
|
+
'menu.insert': 'Quote in composer',
|
|
212
|
+
'menu.inspect': 'Open record details',
|
|
213
|
+
'menu.fork': 'Fork from here',
|
|
214
|
+
'menu.forkHint': 'inclusive',
|
|
215
|
+
'menu.export': 'Export trajectory JSON',
|
|
216
|
+
'menu.session': 'Copy session ID',
|
|
217
|
+
'menu.panel': 'Open the Power-Up page',
|
|
218
|
+
'menu.noFork': 'No fork position available for this record',
|
|
219
|
+
'menu.blank': 'Session actions',
|
|
220
|
+
'toast.copied': 'Copied',
|
|
221
|
+
'toast.copyFailed': 'Copy failed',
|
|
222
|
+
'toast.inserted': 'Inserted into the composer',
|
|
223
|
+
'toast.insertFallback': 'Composer unavailable; copied to clipboard',
|
|
224
|
+
'toast.forked': 'Session forked',
|
|
225
|
+
'toast.forkFailed': 'Fork failed: {message}',
|
|
226
|
+
'toast.exported': 'Trajectory exported',
|
|
227
|
+
'toast.exportFailed': 'Export failed: {message}',
|
|
228
|
+
'toast.empty': 'This record has no text to copy',
|
|
229
|
+
'toast.actionFailed': 'Action failed: {message}',
|
|
230
|
+
'toast.sessionCopied': 'Session ID copied',
|
|
231
|
+
'toast.forkUnavailable': 'This host does not expose the fork API',
|
|
232
|
+
'toast.noSession': 'No session is open',
|
|
233
|
+
'update.button': 'Update version',
|
|
234
|
+
'update.title': 'Update plugin',
|
|
235
|
+
'update.subtitle': 'Choose a version; the old one is removed and the new one installed.',
|
|
236
|
+
'update.current': 'Current version',
|
|
237
|
+
'update.versionLabel': 'Version',
|
|
238
|
+
'update.versionPlaceholder': 'Type a version or pick one from the list',
|
|
239
|
+
'update.loadingVersions': 'Reading versions from the registry…',
|
|
240
|
+
'update.loading': 'Reading the plugin…',
|
|
241
|
+
'update.chooseVersion': 'Choose',
|
|
242
|
+
'update.versionsFailed': 'Could not read versions: {message}',
|
|
243
|
+
'update.versionsEmpty': 'The registry returned no versions',
|
|
244
|
+
'update.latest': 'latest',
|
|
245
|
+
'update.retry': 'Retry',
|
|
246
|
+
'update.registry': 'Registry',
|
|
247
|
+
'update.registryDefault': 'Default registry',
|
|
248
|
+
'update.registryMirror': 'Mainland China mirror',
|
|
249
|
+
'update.registryCustom': 'Custom address',
|
|
250
|
+
'update.registryFrom': 'Versions came from {registry}',
|
|
251
|
+
'update.start': 'Update',
|
|
252
|
+
'update.cancel': 'Cancel',
|
|
253
|
+
'update.close': 'Close',
|
|
254
|
+
'update.step.checking': 'Checking the version…',
|
|
255
|
+
'update.step.removing': 'Removing the old version…',
|
|
256
|
+
'update.step.installing': 'Installing {version}…',
|
|
257
|
+
'update.step.enabling': 'Enabling…',
|
|
258
|
+
'update.log': 'Install output',
|
|
259
|
+
'update.enableTitle': 'Installed. Enable it now?',
|
|
260
|
+
'update.enableDesc': 'The new version loads only after it is enabled.',
|
|
261
|
+
'update.enableNow': 'Enable now',
|
|
262
|
+
'update.enableLater': 'Enable later',
|
|
263
|
+
'update.enableFailed': 'Could not enable: {message}',
|
|
264
|
+
'update.restartTitle': 'Restart DSH now?',
|
|
265
|
+
'update.restartDesc': 'Restarting closes and reopens DSH; running tasks are interrupted.',
|
|
266
|
+
'update.restartNow': 'Restart now',
|
|
267
|
+
'update.restartLater': 'Restart later',
|
|
268
|
+
'update.restartUnsupported': 'This runtime cannot restart itself: {reason}',
|
|
269
|
+
'update.restarting': 'DSH is restarting; this window will close…',
|
|
270
|
+
'update.restartFailed': 'Restart request failed: {message}',
|
|
271
|
+
'update.doneTitle': 'Update complete',
|
|
272
|
+
'update.doneInstalled': '{name} is now on {version}.',
|
|
273
|
+
'update.doneDisabled': '{name} {version} is installed but not enabled.',
|
|
274
|
+
'update.doneEnabled': 'The new version is enabled.',
|
|
275
|
+
'update.doneRestart': 'Some changes apply only after DSH restarts.',
|
|
276
|
+
'update.doneButton': 'Done',
|
|
277
|
+
'update.errorTitle': 'Update failed',
|
|
278
|
+
'update.selfTitle': 'Update Power-Up with Add plugin',
|
|
279
|
+
'update.selfDesc': 'Updating Power-Up removes this plugin first, which interrupts this flow.',
|
|
280
|
+
'update.notRemovable': 'DSH manages this plugin; it cannot be updated here.',
|
|
281
|
+
'update.notBundle': 'This package is not a DSH plugin bundle.',
|
|
282
|
+
'update.alreadyInstalled': 'This package is already installed; remove the old version first.',
|
|
283
|
+
'update.notFound': 'That version is not available from the registry.',
|
|
284
|
+
'update.cancelled': 'The update was cancelled.',
|
|
285
|
+
'update.installedChip': 'Not installed',
|
|
286
|
+
'update.unknown': 'Unknown error',
|
|
287
|
+
'update.invalidVersion': 'Enter a valid version, for example 1.2.3.',
|
|
288
|
+
'update.stopProfile': 'This profile cannot hot-remove the plugin; restart DSH and try again.',
|
|
289
|
+
'update.unsupportedTitle': 'Cannot update automatically',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
function detectLocale() {
|
|
294
|
+
try {
|
|
295
|
+
return String(navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
296
|
+
} catch {
|
|
297
|
+
return 'en';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function format(template, params) {
|
|
302
|
+
if (params === undefined) return template;
|
|
303
|
+
return template.replace(/\{(\w+)\}/g, (match, key) =>
|
|
304
|
+
Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : match);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Standalone translator used before/without the host locale service. */
|
|
308
|
+
function makeFallbackTranslate() {
|
|
309
|
+
let locale = detectLocale();
|
|
310
|
+
return (key, params) => {
|
|
311
|
+
const table = DICT[locale] ?? DICT.en;
|
|
312
|
+
return format(table[key] ?? DICT.en[key] ?? key, params);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ------------------------------------------------------------------
|
|
317
|
+
// Settings store (localStorage, per browser)
|
|
318
|
+
// ------------------------------------------------------------------
|
|
319
|
+
function readSettings() {
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(localStorage.getItem(SETTINGS_KEY) || 'null');
|
|
322
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
323
|
+
return { enabled: parsed.enabled !== false };
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
/* Private mode or a corrupted value: use the default. */
|
|
327
|
+
}
|
|
328
|
+
return { enabled: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let settingsValue = readSettings();
|
|
332
|
+
const settingsListeners = new Set();
|
|
333
|
+
|
|
334
|
+
function getSettings() {
|
|
335
|
+
return settingsValue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function setSettings(patch) {
|
|
339
|
+
settingsValue = { ...settingsValue, ...patch };
|
|
340
|
+
try {
|
|
341
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settingsValue));
|
|
342
|
+
} catch {
|
|
343
|
+
/* The in-memory value still works for this page. */
|
|
344
|
+
}
|
|
345
|
+
for (const listener of [...settingsListeners]) {
|
|
346
|
+
try {
|
|
347
|
+
listener();
|
|
348
|
+
} catch {
|
|
349
|
+
/* A listener must not break the toggle. */
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function subscribeSettings(listener) {
|
|
355
|
+
settingsListeners.add(listener);
|
|
356
|
+
return () => settingsListeners.delete(listener);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function useSettings() {
|
|
360
|
+
const [value, setValue] = useState(getSettings);
|
|
361
|
+
useEffect(() => subscribeSettings(() => setValue(getSettings())), []);
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ------------------------------------------------------------------
|
|
366
|
+
// Session-scoped bridge
|
|
367
|
+
// ------------------------------------------------------------------
|
|
368
|
+
/**
|
|
369
|
+
* Latest values published by the hidden header utility. `shell.overlay`
|
|
370
|
+
* is root-scoped and cannot read session hooks, so the bridge is the only
|
|
371
|
+
* place where `sessionId`, `useTrajectory` and `inputActions` meet.
|
|
372
|
+
*/
|
|
373
|
+
const bridge = {
|
|
374
|
+
token: null,
|
|
375
|
+
sessionId: undefined,
|
|
376
|
+
trajectory: undefined,
|
|
377
|
+
inputActions: undefined,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
function PowerUpBridge({ sessionId, useTrajectory, inputActions }) {
|
|
381
|
+
const snapshot = typeof useTrajectory === 'function' ? useTrajectory((value) => value) : undefined;
|
|
382
|
+
useLayoutEffect(() => {
|
|
383
|
+
const token = {};
|
|
384
|
+
bridge.token = token;
|
|
385
|
+
bridge.sessionId = sessionId;
|
|
386
|
+
bridge.trajectory = snapshot;
|
|
387
|
+
bridge.inputActions = inputActions;
|
|
388
|
+
return () => {
|
|
389
|
+
if (bridge.token === token) {
|
|
390
|
+
bridge.token = null;
|
|
391
|
+
bridge.sessionId = undefined;
|
|
392
|
+
bridge.trajectory = undefined;
|
|
393
|
+
bridge.inputActions = undefined;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}, [sessionId, snapshot, inputActions]);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ------------------------------------------------------------------
|
|
401
|
+
// Icons (16-grid, currentColor, host stroke convention)
|
|
402
|
+
// ------------------------------------------------------------------
|
|
403
|
+
const ICON_VIEW_BOX = '0 0 16 16';
|
|
404
|
+
|
|
405
|
+
function Icon({ size = 16, children }) {
|
|
406
|
+
return h('svg', {
|
|
407
|
+
width: size,
|
|
408
|
+
height: size,
|
|
409
|
+
viewBox: ICON_VIEW_BOX,
|
|
410
|
+
fill: 'none',
|
|
411
|
+
'aria-hidden': 'true',
|
|
412
|
+
focusable: 'false',
|
|
413
|
+
}, children);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function PowerUpLineIcon({ size = 16 }) {
|
|
417
|
+
return h(Icon, { size },
|
|
418
|
+
h('rect', {
|
|
419
|
+
x: '1.65', y: '1.65', width: '12.7', height: '12.7', rx: '3.8',
|
|
420
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.3',
|
|
421
|
+
}),
|
|
422
|
+
h('path', {
|
|
423
|
+
d: 'M4.2 5.8 8 2.7l3.8 3.1M4.2 10.9 8 7.8l3.8 3.1',
|
|
424
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.3',
|
|
425
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
426
|
+
}),
|
|
427
|
+
h('path', {
|
|
428
|
+
d: 'M5.8 13.1h4.4', fill: 'none', stroke: 'currentColor',
|
|
429
|
+
strokeWidth: '1.3', strokeLinecap: 'round',
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function BrandMark({ size = 56 }) {
|
|
435
|
+
return h('svg', {
|
|
436
|
+
width: size, height: size, viewBox: '0 0 256 256', fill: 'none', 'aria-hidden': 'true',
|
|
437
|
+
},
|
|
438
|
+
h('defs', null,
|
|
439
|
+
h('linearGradient', {
|
|
440
|
+
id: 'dsh-powerup-brand-bg', x1: '32', y1: '24', x2: '224', y2: '232',
|
|
441
|
+
gradientUnits: 'userSpaceOnUse',
|
|
442
|
+
},
|
|
443
|
+
h('stop', { stopColor: '#8B5CF6' }),
|
|
444
|
+
h('stop', { offset: '0.55', stopColor: '#6D3BE0' }),
|
|
445
|
+
h('stop', { offset: '1', stopColor: '#4C1D95' }),
|
|
446
|
+
),
|
|
447
|
+
h('linearGradient', {
|
|
448
|
+
id: 'dsh-powerup-brand-chevron', x1: '78', y1: '56', x2: '178', y2: '178',
|
|
449
|
+
gradientUnits: 'userSpaceOnUse',
|
|
450
|
+
},
|
|
451
|
+
h('stop', { stopColor: '#FFFFFF' }),
|
|
452
|
+
h('stop', { offset: '1', stopColor: '#D8FFF4' }),
|
|
453
|
+
),
|
|
454
|
+
),
|
|
455
|
+
h('rect', { x: '16', y: '16', width: '224', height: '224', rx: '56', fill: 'url(#dsh-powerup-brand-bg)' }),
|
|
456
|
+
h('path', {
|
|
457
|
+
d: 'M78 102 128 56l50 46', stroke: 'url(#dsh-powerup-brand-chevron)',
|
|
458
|
+
strokeWidth: '22', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
459
|
+
}),
|
|
460
|
+
h('path', {
|
|
461
|
+
d: 'M78 178 128 132l50 46', stroke: '#9DF2DB',
|
|
462
|
+
strokeWidth: '22', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
463
|
+
}),
|
|
464
|
+
h('path', { d: 'M96 212h64', stroke: '#FFC94D', strokeWidth: '10', strokeLinecap: 'round' }),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function CopyIcon({ size = 16 }) {
|
|
469
|
+
return h(Icon, { size },
|
|
470
|
+
h('rect', {
|
|
471
|
+
x: '5.2', y: '5.2', width: '8.3', height: '8.3', rx: '2',
|
|
472
|
+
stroke: 'currentColor', strokeWidth: '1.3',
|
|
473
|
+
}),
|
|
474
|
+
h('path', {
|
|
475
|
+
d: 'M10.8 5.2V4.1A1.6 1.6 0 0 0 9.2 2.5H4.1A1.6 1.6 0 0 0 2.5 4.1v5.1a1.6 1.6 0 0 0 1.6 1.6h1.1',
|
|
476
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function QuoteIcon({ size = 16 }) {
|
|
482
|
+
return h(Icon, { size },
|
|
483
|
+
h('path', {
|
|
484
|
+
d: 'M5.4 4.1C4.1 4.8 3.3 5.9 3.3 7.4v4.3h4.2V7.4H5.6c0-.8.4-1.4 1.1-1.8L5.4 4.1Zm6 0C10.1 4.8 9.3 5.9 9.3 7.4v4.3h4.2V7.4h-1.9c0-.8.4-1.4 1.1-1.8l-1.3-1.5Z',
|
|
485
|
+
fill: 'currentColor',
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function ComposerIcon({ size = 16 }) {
|
|
491
|
+
return h(Icon, { size },
|
|
492
|
+
h('path', {
|
|
493
|
+
d: 'M8 2.7v6.5M5.4 6.9 8 9.5l2.6-2.6',
|
|
494
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
495
|
+
}),
|
|
496
|
+
h('path', {
|
|
497
|
+
d: 'M3 11.7v.8A1.5 1.5 0 0 0 4.5 14h7a1.5 1.5 0 0 0 1.5-1.5v-.8',
|
|
498
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function InspectIcon({ size = 16 }) {
|
|
504
|
+
return h(Icon, { size },
|
|
505
|
+
h('circle', {
|
|
506
|
+
cx: '7.1', cy: '7.1', r: '3.7', stroke: 'currentColor', strokeWidth: '1.3',
|
|
507
|
+
}),
|
|
508
|
+
h('path', {
|
|
509
|
+
d: 'm9.9 9.9 3 3', stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function ForkIcon({ size = 16 }) {
|
|
515
|
+
return h(Icon, { size },
|
|
516
|
+
h('circle', { cx: '4', cy: '3.5', r: '1.5', stroke: 'currentColor', strokeWidth: '1.2' }),
|
|
517
|
+
h('circle', { cx: '4', cy: '12.5', r: '1.5', stroke: 'currentColor', strokeWidth: '1.2' }),
|
|
518
|
+
h('circle', { cx: '12', cy: '8', r: '1.5', stroke: 'currentColor', strokeWidth: '1.2' }),
|
|
519
|
+
h('path', {
|
|
520
|
+
d: 'M4 5v6M4 8h4.2a2.8 2.8 0 0 0 2.8-2.8V6.5',
|
|
521
|
+
stroke: 'currentColor', strokeWidth: '1.2', strokeLinecap: 'round',
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function ExportIcon({ size = 16 }) {
|
|
527
|
+
return h(Icon, { size },
|
|
528
|
+
h('path', {
|
|
529
|
+
d: 'M8 2.6v6.8M5.3 6.7 8 9.4l2.7-2.7',
|
|
530
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
531
|
+
}),
|
|
532
|
+
h('path', {
|
|
533
|
+
d: 'M3 11.5v.9A1.6 1.6 0 0 0 4.6 14h6.8a1.6 1.6 0 0 0 1.6-1.6v-.9',
|
|
534
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function HashIcon({ size = 16 }) {
|
|
540
|
+
return h(Icon, { size },
|
|
541
|
+
h('path', {
|
|
542
|
+
d: 'M6.2 2.6 5 13.4M11 2.6 9.8 13.4M3.2 6.2h9.6M2.8 9.8h9.6',
|
|
543
|
+
stroke: 'currentColor', strokeWidth: '1.2', strokeLinecap: 'round',
|
|
544
|
+
}),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function LayoutIcon({ size = 16 }) {
|
|
549
|
+
return h(Icon, { size },
|
|
550
|
+
h('rect', {
|
|
551
|
+
x: '2.4', y: '2.8', width: '11.2', height: '10.4', rx: '1.8',
|
|
552
|
+
stroke: 'currentColor', strokeWidth: '1.3',
|
|
553
|
+
}),
|
|
554
|
+
h('path', { d: 'M6.2 2.8v10.4', stroke: 'currentColor', strokeWidth: '1.3' }),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function UpdateIcon({ size = 16 }) {
|
|
559
|
+
return h(Icon, { size },
|
|
560
|
+
h('path', {
|
|
561
|
+
d: 'M8 10.6V3.1M5.2 5.9 8 3.1l2.8 2.8',
|
|
562
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
563
|
+
}),
|
|
564
|
+
h('path', {
|
|
565
|
+
d: 'M2.9 10.4v1A1.6 1.6 0 0 0 4.5 13h7a1.6 1.6 0 0 0 1.6-1.6v-1',
|
|
566
|
+
stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
567
|
+
}),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function CheckIcon({ size = 16 }) {
|
|
572
|
+
return h(Icon, { size },
|
|
573
|
+
h('path', {
|
|
574
|
+
d: 'm3.2 8.4 3 3 6.6-6.8',
|
|
575
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.6',
|
|
576
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
577
|
+
}),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function WarnIcon({ size = 16 }) {
|
|
582
|
+
return h(Icon, { size },
|
|
583
|
+
h('path', {
|
|
584
|
+
d: 'M8 2.6 14 13H2L8 2.6Z',
|
|
585
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.3', strokeLinejoin: 'round',
|
|
586
|
+
}),
|
|
587
|
+
h('path', {
|
|
588
|
+
d: 'M8 6.3v3.1M8 11.4v.1',
|
|
589
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round',
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ------------------------------------------------------------------
|
|
595
|
+
// CSS — host tokens only (except the brand SVG artwork)
|
|
596
|
+
// ------------------------------------------------------------------
|
|
597
|
+
const CSS = `
|
|
598
|
+
.dsh-powerup-page{box-sizing:border-box;height:100%;overflow-y:auto;padding:28px 24px 52px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.55;--pu-accent:var(--dsw-alias-brand-primary)}
|
|
599
|
+
.dsh-powerup-page *{box-sizing:border-box}
|
|
600
|
+
.dsh-powerup-shell{width:100%;max-width:820px;margin:0 auto}
|
|
601
|
+
.dsh-powerup-hero{display:flex;align-items:center;gap:16px;padding:4px 0 24px}
|
|
602
|
+
.dsh-powerup-hero-art{flex:none;display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:17px;box-shadow:var(--dsw-elevation-lv2,0 8px 22px rgba(15,23,42,.18))}
|
|
603
|
+
.dsh-powerup-hero-copy{min-width:0}
|
|
604
|
+
.dsh-powerup-hero h1{margin:0;font-size:22px;line-height:1.25;font-weight:650;letter-spacing:-.01em}
|
|
605
|
+
.dsh-powerup-hero p{margin:5px 0 0;color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:1.5}
|
|
606
|
+
.dsh-powerup-card{margin:0 0 14px;padding:16px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}
|
|
607
|
+
.dsh-powerup-card-head{display:flex;align-items:flex-start;gap:12px}
|
|
608
|
+
.dsh-powerup-card-icon{flex:none;display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
|
|
609
|
+
.dsh-powerup-card-copy{min-width:0;flex:1 1 auto}
|
|
610
|
+
.dsh-powerup-card-copy h2{margin:0;font-size:14px;line-height:1.35;font-weight:650}
|
|
611
|
+
.dsh-powerup-card-copy p{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}
|
|
612
|
+
.dsh-powerup-switch{flex:none;display:inline-flex;align-items:center;gap:7px;padding:3px 4px 3px 6px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:18px;cursor:pointer;white-space:nowrap}
|
|
613
|
+
.dsh-powerup-switch:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}
|
|
614
|
+
.dsh-powerup-switch:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
615
|
+
.dsh-powerup-switch-track{position:relative;display:inline-block;flex:none;width:32px;height:17px;border-radius:999px;background:var(--dsw-alias-border-l3);transition:background .14s ease}
|
|
616
|
+
.dsh-powerup-switch-thumb{position:absolute;top:2px;left:2px;display:block;width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-switch-thumb,var(--dsw-alias-bg-layer-1));box-shadow:0 1px 2px rgba(15,23,42,.18);transition:transform .14s ease,background .14s ease}
|
|
617
|
+
.dsh-powerup-switch[aria-checked=true]{color:var(--dsw-alias-label-primary);font-weight:600}
|
|
618
|
+
.dsh-powerup-switch[aria-checked=true] .dsh-powerup-switch-track{background:var(--dsw-alias-brand-primary)}
|
|
619
|
+
.dsh-powerup-switch[aria-checked=true] .dsh-powerup-switch-thumb{background:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-bg-layer-1));transform:translateX(15px)}
|
|
620
|
+
.dsh-powerup-section-title{margin:0 0 10px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
|
|
621
|
+
.dsh-powerup-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin:0;padding:0;list-style:none}
|
|
622
|
+
.dsh-powerup-action{display:flex;align-items:flex-start;gap:8px;min-width:0;padding:7px 8px;border-radius:8px}
|
|
623
|
+
.dsh-powerup-action-icon{flex:none;display:flex;align-items:center;justify-content:center;width:20px;height:20px;margin-top:1px;color:var(--dsw-alias-label-tertiary)}
|
|
624
|
+
.dsh-powerup-action-copy{min-width:0}
|
|
625
|
+
.dsh-powerup-action-name{display:block;font-size:12px;line-height:1.4;color:var(--dsw-alias-label-primary)}
|
|
626
|
+
.dsh-powerup-action-hint{display:block;margin-top:1px;font-size:10.5px;line-height:1.45;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
|
|
627
|
+
.dsh-powerup-steps{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:7px}
|
|
628
|
+
.dsh-powerup-step{display:flex;align-items:flex-start;gap:9px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}
|
|
629
|
+
.dsh-powerup-step-num{flex:none;display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;margin-top:1px;border-radius:50%;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}
|
|
630
|
+
.dsh-powerup-foot{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:2px 2px 0;color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:1.5}
|
|
631
|
+
.dsh-powerup-dot{width:3px;height:3px;border-radius:50%;background:var(--dsw-alias-label-caption);opacity:.6}
|
|
632
|
+
.dsh-powerup-menu{position:fixed;z-index:1000;min-width:232px;max-width:300px;max-height:min(70vh,430px);overflow-y:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-1));box-shadow:var(--dsw-elevation-prominent,0 16px 40px rgba(15,23,42,.24),0 2px 8px rgba(15,23,42,.1));color:var(--dsw-alias-label-primary);font-size:12.5px;line-height:1.4;pointer-events:auto;outline:none;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
|
|
633
|
+
.dsh-powerup-menu-head{display:flex;align-items:center;gap:6px;padding:5px 7px 6px;color:var(--dsw-alias-label-tertiary);font-size:10.5px;font-weight:600;letter-spacing:.03em}
|
|
634
|
+
.dsh-powerup-menu-head-icon{display:flex;align-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
635
|
+
.dsh-powerup-menu-kind{margin-left:auto;max-width:112px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 5px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-weight:500;line-height:16px}
|
|
636
|
+
.dsh-powerup-menu-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1)}
|
|
637
|
+
.dsh-powerup-item{display:flex;align-items:center;gap:8px;width:100%;min-height:30px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;line-height:30px;text-align:left;cursor:pointer;white-space:nowrap}
|
|
638
|
+
.dsh-powerup-item:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}
|
|
639
|
+
.dsh-powerup-item:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
640
|
+
.dsh-powerup-item:disabled{color:var(--dsw-alias-label-tertiary);opacity:.55;cursor:default}
|
|
641
|
+
.dsh-powerup-item-icon{flex:none;display:flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
642
|
+
.dsh-powerup-item:hover:not(:disabled) .dsh-powerup-item-icon{color:var(--dsw-alias-label-primary)}
|
|
643
|
+
.dsh-powerup-item-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
644
|
+
.dsh-powerup-item-hint{flex:none;color:var(--dsw-alias-label-caption);font-size:10.5px}
|
|
645
|
+
.dsh-powerup-menu-foot{display:flex;align-items:center;gap:6px;margin-top:4px;padding:5px 7px 3px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-caption);font-size:10px;line-height:1.4}
|
|
646
|
+
.dsh-powerup-menu-foot-id{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--dsw-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace)}
|
|
647
|
+
.dsh-powerup-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);z-index:1001;max-width:min(440px,calc(100vw - 32px));padding:7px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgba(15,23,42,.18));font-size:12px;line-height:1.45;pointer-events:none;animation:dsh-powerup-toast-in .14s ease-out}
|
|
648
|
+
@keyframes dsh-powerup-toast-in{from{opacity:0;transform:translate(-50%,5px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
649
|
+
.dsh-powerup-update-button{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;color:var(--dsw-alias-label-caption);cursor:pointer}
|
|
650
|
+
.dsh-powerup-update-button:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary)}
|
|
651
|
+
.dsh-powerup-update-button:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
652
|
+
.dsh-powerup-update-mask{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb,var(--dsw-alias-bg-base) 62%,transparent);pointer-events:auto}
|
|
653
|
+
.dsh-powerup-update-dialog{display:flex;flex-direction:column;width:min(560px,100%);max-height:min(760px,calc(100vh - 48px));border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-1));box-shadow:var(--dsw-elevation-prominent,0 24px 64px rgba(15,23,42,.28));color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;overflow:hidden}
|
|
654
|
+
.dsh-powerup-update-head{display:flex;align-items:center;gap:11px;padding:16px 18px 12px;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
655
|
+
.dsh-powerup-update-icon{flex:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
|
|
656
|
+
.dsh-powerup-update-heading{flex:1 1 auto;min-width:0}
|
|
657
|
+
.dsh-powerup-update-heading h2{margin:0;font-size:15px;font-weight:650;line-height:1.3}
|
|
658
|
+
.dsh-powerup-update-heading p{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
659
|
+
.dsh-powerup-update-close{flex:none;width:26px;height:26px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:18px;line-height:1;cursor:pointer}
|
|
660
|
+
.dsh-powerup-update-close:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary)}
|
|
661
|
+
.dsh-powerup-update-close:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
662
|
+
.dsh-powerup-update-body{display:flex;flex-direction:column;gap:14px;min-height:0;padding:16px 18px;overflow-y:auto;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
|
|
663
|
+
.dsh-powerup-update-name{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11.5px;line-height:1.4}
|
|
664
|
+
.dsh-powerup-update-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
665
|
+
.dsh-powerup-update-chip{padding:0 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:10.5px;line-height:18px}
|
|
666
|
+
.dsh-powerup-update-label{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
|
|
667
|
+
.dsh-powerup-update-field{display:flex;flex-direction:column;gap:6px}
|
|
668
|
+
.dsh-powerup-update-row{display:flex;gap:8px}
|
|
669
|
+
.dsh-powerup-update-input{flex:1 1 auto;min-width:0;height:38px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;outline:none}
|
|
670
|
+
.dsh-powerup-update-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:inset 0 0 0 .5px var(--dsw-alias-state-business-primary)}
|
|
671
|
+
.dsh-powerup-update-toggle{flex:none;height:38px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
|
|
672
|
+
.dsh-powerup-update-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-3));color:var(--dsw-alias-label-primary)}
|
|
673
|
+
.dsh-powerup-update-toggle:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
674
|
+
.dsh-powerup-update-list{display:flex;flex-direction:column;max-height:220px;overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
|
|
675
|
+
.dsh-powerup-update-version{display:flex;align-items:center;gap:8px;width:100%;padding:7px 11px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;text-align:left;cursor:pointer}
|
|
676
|
+
.dsh-powerup-update-version:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}
|
|
677
|
+
.dsh-powerup-update-version[aria-selected=true]{background:var(--dsw-alias-bg-layer-2)}
|
|
678
|
+
.dsh-powerup-update-version:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
679
|
+
.dsh-powerup-update-tag{margin-left:auto;padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:17px}
|
|
680
|
+
.dsh-powerup-update-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
681
|
+
.dsh-powerup-update-spinner{flex:none;width:14px;height:14px;border:2px solid var(--dsw-alias-border-l3);border-top-color:var(--dsw-alias-brand-primary);border-radius:50%;animation:dsh-powerup-spin .8s linear infinite}
|
|
682
|
+
@keyframes dsh-powerup-spin{to{transform:rotate(360deg)}}
|
|
683
|
+
@media (prefers-reduced-motion:reduce){.dsh-powerup-update-spinner{animation:none}}
|
|
684
|
+
.dsh-powerup-update-log{margin:0;max-height:140px;overflow:auto;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
685
|
+
.dsh-powerup-update-error{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:1.5}
|
|
686
|
+
.dsh-powerup-update-error svg{flex:none;margin-top:1px}
|
|
687
|
+
.dsh-powerup-update-error span{min-width:0;overflow-wrap:anywhere}
|
|
688
|
+
.dsh-powerup-update-note{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.55}
|
|
689
|
+
.dsh-powerup-update-registry{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
690
|
+
.dsh-powerup-update-select{height:30px;max-width:100%;padding:0 8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:7px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;outline:none}
|
|
691
|
+
.dsh-powerup-update-select:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
692
|
+
.dsh-powerup-update-foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px 16px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
693
|
+
.dsh-powerup-update-primary{height:32px;padding:0 14px;border:0;border-radius:8px;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-bg-layer-1));font:inherit;font-size:12.5px;font-weight:600;cursor:pointer}
|
|
694
|
+
.dsh-powerup-update-primary:hover:not(:disabled){opacity:.9}
|
|
695
|
+
.dsh-powerup-update-primary:disabled{opacity:.5;cursor:default}
|
|
696
|
+
.dsh-powerup-update-primary:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
697
|
+
.dsh-powerup-update-ghost{height:32px;padding:0 14px;border:1px solid var(--dsw-alias-border-l3);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;cursor:pointer}
|
|
698
|
+
.dsh-powerup-update-ghost:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}
|
|
699
|
+
.dsh-powerup-update-ghost:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
700
|
+
@media (max-width:680px){.dsh-powerup-page{padding:20px 16px 42px}.dsh-powerup-actions{grid-template-columns:minmax(0,1fr)}.dsh-powerup-hero-art{width:54px;height:54px}.dsh-powerup-hero h1{font-size:19px}.dsh-powerup-card-head{flex-wrap:wrap}.dsh-powerup-switch{margin-left:auto}.dsh-powerup-update-mask{padding:12px}.dsh-powerup-update-dialog{max-height:calc(100vh - 24px)}}
|
|
701
|
+
`;
|
|
702
|
+
|
|
703
|
+
function installStyles(ctx) {
|
|
704
|
+
try {
|
|
705
|
+
const style = document.createElement('style');
|
|
706
|
+
style.setAttribute('data-dsh-powerup', '');
|
|
707
|
+
style.textContent = CSS;
|
|
708
|
+
document.head.appendChild(style);
|
|
709
|
+
ctx.effect(() => () => style.remove(), 'dsh-powerup: styles');
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.warn('[dsh-powerup] style injection failed:', error?.message ?? error);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ------------------------------------------------------------------
|
|
716
|
+
// Clipboard + trajectory helpers
|
|
717
|
+
// ------------------------------------------------------------------
|
|
718
|
+
function shortId(id) {
|
|
719
|
+
return typeof id === 'string' && id.length > 0 ? id.slice(0, 8) : '—';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function copyText(text) {
|
|
723
|
+
if (typeof text !== 'string' || text === '') throw new Error('empty');
|
|
724
|
+
try {
|
|
725
|
+
if (navigator.clipboard !== undefined && typeof navigator.clipboard.writeText === 'function') {
|
|
726
|
+
await navigator.clipboard.writeText(text);
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
/* Fall through to the textarea path. */
|
|
731
|
+
}
|
|
732
|
+
const area = document.createElement('textarea');
|
|
733
|
+
area.value = text;
|
|
734
|
+
area.setAttribute('readonly', '');
|
|
735
|
+
area.style.position = 'fixed';
|
|
736
|
+
area.style.opacity = '0';
|
|
737
|
+
area.style.pointerEvents = 'none';
|
|
738
|
+
document.body.appendChild(area);
|
|
739
|
+
try {
|
|
740
|
+
area.select();
|
|
741
|
+
const ok = typeof document.execCommand === 'function' ? document.execCommand('copy') : false;
|
|
742
|
+
if (!ok) throw new Error('copy-command-failed');
|
|
743
|
+
return true;
|
|
744
|
+
} finally {
|
|
745
|
+
area.remove();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Decode the ledger row identity into the trajectory record id parts. */
|
|
750
|
+
function rowKeyParts(row) {
|
|
751
|
+
const raw = row.getAttribute('data-trajectory-row-key');
|
|
752
|
+
if (typeof raw !== 'string' || raw === '') return [];
|
|
753
|
+
try {
|
|
754
|
+
return decodeURIComponent(raw).split('\u0000');
|
|
755
|
+
} catch {
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** The full visible content of one ledger row. */
|
|
761
|
+
function rowText(row) {
|
|
762
|
+
const cell = row.querySelector('td:nth-child(2)');
|
|
763
|
+
if (cell === null) return '';
|
|
764
|
+
const titled = cell.querySelector('span[title]');
|
|
765
|
+
const full = titled?.getAttribute('title');
|
|
766
|
+
if (typeof full === 'string' && full.trim() !== '') return full.trim();
|
|
767
|
+
return (cell.innerText || cell.textContent || '').trim();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function rowKind(row) {
|
|
771
|
+
return row.getAttribute('data-kind') || '';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function rowKindLabel(row) {
|
|
775
|
+
const label = row.querySelector('[data-role-kind]')?.textContent?.trim();
|
|
776
|
+
return label || '';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function markdownQuote(row, t) {
|
|
780
|
+
const text = rowText(row);
|
|
781
|
+
if (text === '') return '';
|
|
782
|
+
const kind = rowKindLabel(row) || t('menu.record');
|
|
783
|
+
const quoted = text.split('\n').map((line) => `> ${line}`).join('\n');
|
|
784
|
+
return `> **${kind}**\n${quoted}`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function containsCallId(value, callId, seen, depth) {
|
|
788
|
+
if (depth > 9 || value === null || typeof value !== 'object') return false;
|
|
789
|
+
if (seen.has(value)) return false;
|
|
790
|
+
seen.add(value);
|
|
791
|
+
if (value.callId === callId) return true;
|
|
792
|
+
for (const key of Object.keys(value)) {
|
|
793
|
+
if (key === 'schema' || key === 'result' || key === 'output') continue;
|
|
794
|
+
if (containsCallId(value[key], callId, seen, depth + 1)) return true;
|
|
795
|
+
}
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function findCallSeq(nodes, callId) {
|
|
800
|
+
if (!Array.isArray(nodes) || typeof callId !== 'string' || callId === '') return null;
|
|
801
|
+
for (const node of nodes) {
|
|
802
|
+
if (node === null || typeof node !== 'object') continue;
|
|
803
|
+
if (containsCallId(node, callId, new Set(), 0) && Number.isFinite(node.seq)) return node.seq;
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Resolve the inclusive event seq a row can be forked at. The row key is
|
|
810
|
+
* the trajectory package's own stable identity: `kind\0seq\0N`,
|
|
811
|
+
* `kind\0call\0ID`, or `assistant\0turn\0T\0step\0S`.
|
|
812
|
+
*/
|
|
813
|
+
function resolveForkSeq(row, snapshot) {
|
|
814
|
+
const parts = rowKeyParts(row);
|
|
815
|
+
if (parts.length === 0) return null;
|
|
816
|
+
// Assistant records use the "assistant", turn, step record id (see
|
|
817
|
+
// ui-trajectory's trajectoryRecordId); older/other rows use the
|
|
818
|
+
// "kind", "seq", N or "kind", "call", ID forms.
|
|
819
|
+
if (parts[0] === 'assistant' && parts.length >= 3) {
|
|
820
|
+
const turn = Number(parts[1]);
|
|
821
|
+
const step = Number(parts[2]);
|
|
822
|
+
const requests = Array.isArray(snapshot?.requests) ? snapshot.requests : [];
|
|
823
|
+
const request = requests.find((candidate) =>
|
|
824
|
+
candidate !== null
|
|
825
|
+
&& candidate.purpose === 'assistant'
|
|
826
|
+
&& candidate.turn === turn
|
|
827
|
+
&& candidate.step === step);
|
|
828
|
+
if (request === undefined) return null;
|
|
829
|
+
return Number.isFinite(request.resultSeq) ? request.resultSeq : (Number.isFinite(request.startSeq) ? request.startSeq : null);
|
|
830
|
+
}
|
|
831
|
+
if (parts[1] === 'seq') {
|
|
832
|
+
const seq = Number(parts[2]);
|
|
833
|
+
return Number.isFinite(seq) ? seq : null;
|
|
834
|
+
}
|
|
835
|
+
if (parts[1] === 'call') {
|
|
836
|
+
return findCallSeq(snapshot?.eventNodes, parts[2]);
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function jsonSafe(value, seen) {
|
|
842
|
+
if (value === null || typeof value !== 'object') {
|
|
843
|
+
return typeof value === 'bigint' ? String(value) : value;
|
|
844
|
+
}
|
|
845
|
+
if (seen.has(value)) return '[circular]';
|
|
846
|
+
seen.add(value);
|
|
847
|
+
if (Array.isArray(value)) return value.map((item) => jsonSafe(item, seen));
|
|
848
|
+
if (value instanceof Map) {
|
|
849
|
+
const out = {};
|
|
850
|
+
for (const [key, entry] of value.entries()) out[String(key)] = jsonSafe(entry, seen);
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
if (value instanceof Set) return [...value].map((item) => jsonSafe(item, seen));
|
|
854
|
+
const out = {};
|
|
855
|
+
for (const key of Object.keys(value)) {
|
|
856
|
+
const entry = value[key];
|
|
857
|
+
if (typeof entry === 'function') continue;
|
|
858
|
+
out[key] = jsonSafe(entry, seen);
|
|
859
|
+
}
|
|
860
|
+
return out;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function downloadJson(filename, data) {
|
|
864
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
865
|
+
const url = URL.createObjectURL(blob);
|
|
866
|
+
const anchor = document.createElement('a');
|
|
867
|
+
anchor.href = url;
|
|
868
|
+
anchor.download = filename;
|
|
869
|
+
anchor.style.display = 'none';
|
|
870
|
+
document.body.appendChild(anchor);
|
|
871
|
+
anchor.click();
|
|
872
|
+
anchor.remove();
|
|
873
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function insertIntoComposer(value) {
|
|
877
|
+
const actions = bridge.inputActions;
|
|
878
|
+
if (actions === null || actions === undefined) return false;
|
|
879
|
+
if (typeof actions.captureInsertion !== 'function' || typeof actions.insertText !== 'function') return false;
|
|
880
|
+
try {
|
|
881
|
+
const span = actions.captureInsertion();
|
|
882
|
+
if (actions.insertText(value, span) === true) {
|
|
883
|
+
actions.persistDraft?.();
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
/* Fall back to the clipboard. */
|
|
888
|
+
}
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ------------------------------------------------------------------
|
|
893
|
+
// Overlay: context menu + toast
|
|
894
|
+
// ------------------------------------------------------------------
|
|
895
|
+
function PowerUpOverlay({ ctx }) {
|
|
896
|
+
const settings = useSettings();
|
|
897
|
+
const [menu, setMenu] = useState(null);
|
|
898
|
+
const [toast, setToast] = useState(null);
|
|
899
|
+
const [position, setPosition] = useState(null);
|
|
900
|
+
const menuRef = useRef(null);
|
|
901
|
+
const toastTimer = useRef(null);
|
|
902
|
+
|
|
903
|
+
const t = useCallback((key, params) => translateCurrent(key, params), []);
|
|
904
|
+
|
|
905
|
+
const notify = useCallback((message) => {
|
|
906
|
+
setToast(message);
|
|
907
|
+
if (toastTimer.current !== null) window.clearTimeout(toastTimer.current);
|
|
908
|
+
toastTimer.current = window.setTimeout(() => {
|
|
909
|
+
toastTimer.current = null;
|
|
910
|
+
setToast(null);
|
|
911
|
+
}, 2200);
|
|
912
|
+
}, []);
|
|
913
|
+
|
|
914
|
+
useEffect(() => () => {
|
|
915
|
+
if (toastTimer.current !== null) {
|
|
916
|
+
window.clearTimeout(toastTimer.current);
|
|
917
|
+
toastTimer.current = null;
|
|
918
|
+
}
|
|
919
|
+
}, []);
|
|
920
|
+
|
|
921
|
+
const close = useCallback(() => {
|
|
922
|
+
setMenu(null);
|
|
923
|
+
setPosition(null);
|
|
924
|
+
}, []);
|
|
925
|
+
|
|
926
|
+
const runAction = useCallback(async (item) => {
|
|
927
|
+
if (item === null || item === undefined || item.disabled === true) return;
|
|
928
|
+
try {
|
|
929
|
+
await item.run();
|
|
930
|
+
} catch (error) {
|
|
931
|
+
notify(format(t('toast.actionFailed'), { message: error?.message ?? String(error) }));
|
|
932
|
+
}
|
|
933
|
+
}, [notify, t]);
|
|
934
|
+
|
|
935
|
+
useEffect(() => {
|
|
936
|
+
if (!settings.enabled) {
|
|
937
|
+
close();
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
const onContextMenu = (event) => {
|
|
941
|
+
const target = event.target;
|
|
942
|
+
if (!(target instanceof Element)) return;
|
|
943
|
+
const pane = target.closest('[data-trajectory-scroll]');
|
|
944
|
+
if (pane === null) return;
|
|
945
|
+
if (target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""], a')) return;
|
|
946
|
+
const row = target.closest('tr[data-trajectory-row-key]');
|
|
947
|
+
event.preventDefault();
|
|
948
|
+
event.stopPropagation();
|
|
949
|
+
const snapshot = bridge.trajectory;
|
|
950
|
+
const sessionId = bridge.sessionId;
|
|
951
|
+
const items = row !== null ? buildRowItems(row, snapshot, t, sessionId, ctx, notify) : buildSessionItems(snapshot, sessionId, t, ctx, notify);
|
|
952
|
+
let x = event.clientX;
|
|
953
|
+
let y = event.clientY;
|
|
954
|
+
if (x === 0 && y === 0) {
|
|
955
|
+
const anchor = row ?? pane;
|
|
956
|
+
const rect = anchor.getBoundingClientRect();
|
|
957
|
+
x = rect.left + Math.min(24, rect.width / 2);
|
|
958
|
+
y = rect.top + Math.min(24, rect.height / 2);
|
|
959
|
+
}
|
|
960
|
+
setPosition({ x, y });
|
|
961
|
+
setMenu({
|
|
962
|
+
x,
|
|
963
|
+
y,
|
|
964
|
+
row,
|
|
965
|
+
sessionId,
|
|
966
|
+
title: row !== null ? rowKindLabel(row) || t('menu.record') : t('menu.blank'),
|
|
967
|
+
items,
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
const onPointerDown = (event) => {
|
|
971
|
+
if (menuRef.current !== null && event.target instanceof Node && menuRef.current.contains(event.target)) return;
|
|
972
|
+
close();
|
|
973
|
+
};
|
|
974
|
+
const onKeyDown = (event) => {
|
|
975
|
+
if (event.key === 'Escape') close();
|
|
976
|
+
};
|
|
977
|
+
const onScroll = (event) => {
|
|
978
|
+
if (menuRef.current !== null && event.target instanceof Node && menuRef.current.contains(event.target)) return;
|
|
979
|
+
close();
|
|
980
|
+
};
|
|
981
|
+
const onResize = () => close();
|
|
982
|
+
document.addEventListener('contextmenu', onContextMenu, true);
|
|
983
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
984
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
985
|
+
document.addEventListener('scroll', onScroll, true);
|
|
986
|
+
window.addEventListener('resize', onResize);
|
|
987
|
+
window.addEventListener('blur', onResize);
|
|
988
|
+
return () => {
|
|
989
|
+
document.removeEventListener('contextmenu', onContextMenu, true);
|
|
990
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
991
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
992
|
+
document.removeEventListener('scroll', onScroll, true);
|
|
993
|
+
window.removeEventListener('resize', onResize);
|
|
994
|
+
window.removeEventListener('blur', onResize);
|
|
995
|
+
};
|
|
996
|
+
}, [settings.enabled, close, t, ctx, notify]);
|
|
997
|
+
|
|
998
|
+
useLayoutEffect(() => {
|
|
999
|
+
if (menu === null || menuRef.current === null) return;
|
|
1000
|
+
const rect = menuRef.current.getBoundingClientRect();
|
|
1001
|
+
const maxX = Math.max(MENU_GAP, window.innerWidth - rect.width - MENU_GAP);
|
|
1002
|
+
const maxY = Math.max(MENU_GAP, window.innerHeight - rect.height - MENU_GAP);
|
|
1003
|
+
const x = Math.min(Math.max(MENU_GAP, menu.x), maxX);
|
|
1004
|
+
const y = Math.min(Math.max(MENU_GAP, menu.y), maxY);
|
|
1005
|
+
if (position === null || position.x !== x || position.y !== y) setPosition({ x, y });
|
|
1006
|
+
}, [menu, position]);
|
|
1007
|
+
|
|
1008
|
+
useEffect(() => {
|
|
1009
|
+
if (menu === null || menuRef.current === null) return;
|
|
1010
|
+
const first = menuRef.current.querySelector('[role="menuitem"]:not(:disabled)');
|
|
1011
|
+
first?.focus?.();
|
|
1012
|
+
}, [menu]);
|
|
1013
|
+
|
|
1014
|
+
if (menu === null) {
|
|
1015
|
+
return toast === null ? null : h('div', { className: 'dsh-powerup-toast', role: 'status' }, toast);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const left = position?.x ?? menu.x;
|
|
1019
|
+
const top = position?.y ?? menu.y;
|
|
1020
|
+
return h(React.Fragment, null,
|
|
1021
|
+
h('div', {
|
|
1022
|
+
className: 'dsh-powerup-menu',
|
|
1023
|
+
role: 'menu',
|
|
1024
|
+
'aria-label': t('menu.title'),
|
|
1025
|
+
ref: menuRef,
|
|
1026
|
+
style: { left, top },
|
|
1027
|
+
onKeyDown: (event) => {
|
|
1028
|
+
const items = [...(menuRef.current?.querySelectorAll('[role="menuitem"]:not(:disabled)') ?? [])];
|
|
1029
|
+
if (items.length === 0) return;
|
|
1030
|
+
const active = document.activeElement;
|
|
1031
|
+
const index = items.indexOf(active);
|
|
1032
|
+
if (event.key === 'ArrowDown') {
|
|
1033
|
+
event.preventDefault();
|
|
1034
|
+
items[(index + 1) % items.length]?.focus?.();
|
|
1035
|
+
} else if (event.key === 'ArrowUp') {
|
|
1036
|
+
event.preventDefault();
|
|
1037
|
+
items[index <= 0 ? items.length - 1 : index - 1]?.focus?.();
|
|
1038
|
+
} else if (event.key === 'Home') {
|
|
1039
|
+
event.preventDefault();
|
|
1040
|
+
items[0]?.focus?.();
|
|
1041
|
+
} else if (event.key === 'End') {
|
|
1042
|
+
event.preventDefault();
|
|
1043
|
+
items[items.length - 1]?.focus?.();
|
|
1044
|
+
} else if (event.key === 'Tab') {
|
|
1045
|
+
close();
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
h('div', { className: 'dsh-powerup-menu-head' },
|
|
1050
|
+
h('span', { className: 'dsh-powerup-menu-head-icon' }, h(PowerUpLineIcon, { size: 13 })),
|
|
1051
|
+
h('span', null, t('menu.title')),
|
|
1052
|
+
h('span', { className: 'dsh-powerup-menu-kind', title: menu.title }, menu.title),
|
|
1053
|
+
),
|
|
1054
|
+
...menu.items.map((item, index) => item.type === 'separator'
|
|
1055
|
+
? h('div', { key: `sep-${index}`, className: 'dsh-powerup-menu-sep', role: 'separator' })
|
|
1056
|
+
: h('button', {
|
|
1057
|
+
key: item.id,
|
|
1058
|
+
type: 'button',
|
|
1059
|
+
role: 'menuitem',
|
|
1060
|
+
className: 'dsh-powerup-item',
|
|
1061
|
+
disabled: item.disabled === true,
|
|
1062
|
+
title: item.title,
|
|
1063
|
+
onClick: () => {
|
|
1064
|
+
if (item.disabled === true) return;
|
|
1065
|
+
close();
|
|
1066
|
+
runAction(item);
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
h('span', { className: 'dsh-powerup-item-icon' }, item.icon),
|
|
1070
|
+
h('span', { className: 'dsh-powerup-item-text' }, item.label),
|
|
1071
|
+
item.hint ? h('span', { className: 'dsh-powerup-item-hint' }, item.hint) : null,
|
|
1072
|
+
)),
|
|
1073
|
+
h('div', { className: 'dsh-powerup-menu-foot' },
|
|
1074
|
+
h('span', null, 'Power-Up'),
|
|
1075
|
+
h('span', { className: 'dsh-powerup-dot' }),
|
|
1076
|
+
h('span', { className: 'dsh-powerup-menu-foot-id', title: menu.sessionId ?? '' }, shortId(menu.sessionId)),
|
|
1077
|
+
),
|
|
1078
|
+
toast === null ? null : h('div', { className: 'dsh-powerup-toast', role: 'status' }, toast),
|
|
1079
|
+
),
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ------------------------------------------------------------------
|
|
1084
|
+
// Plugin update: entry button on the built-in Plugins page + dialog
|
|
1085
|
+
// ------------------------------------------------------------------
|
|
1086
|
+
const PLUGIN_REGISTRY_MEMORY_KEY = 'dsh.plugin-manager.install-registry';
|
|
1087
|
+
const UPDATE_API = '/dsh-powerup/api/restart';
|
|
1088
|
+
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
1089
|
+
const REGISTRY_HOSTS = new Map([
|
|
1090
|
+
['registry.npmjs.org', 'update.registryDefault'],
|
|
1091
|
+
['registry.npmmirror.com', 'update.registryMirror'],
|
|
1092
|
+
]);
|
|
1093
|
+
|
|
1094
|
+
/** The npm registry host of a URL, or the URL as written. */
|
|
1095
|
+
function registryHost(url) {
|
|
1096
|
+
try {
|
|
1097
|
+
return new URL(url).host;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return String(url ?? '');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/** A registry URL with one trailing slash, for building metadata URLs. */
|
|
1104
|
+
function normalizeRegistry(url) {
|
|
1105
|
+
const value = String(url ?? '').trim();
|
|
1106
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** A registry value the Host compares as a string; null means "pnpm's own". */
|
|
1110
|
+
function registryValue(choice) {
|
|
1111
|
+
return choice === null || choice === undefined ? null : choice.value;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** The concrete URL to read metadata from. */
|
|
1115
|
+
function registryFetchUrl(choice) {
|
|
1116
|
+
return normalizeRegistry(choice?.url ?? OFFICIAL_REGISTRY);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Read the registry the built-in Add plugin dialog remembers. The store
|
|
1121
|
+
* persists its snapshot under this localStorage key as plain JSON.
|
|
1122
|
+
* @returns {{ kind: string, registry?: string | null, url?: string } | null}
|
|
1123
|
+
*/
|
|
1124
|
+
function readRememberedRegistry() {
|
|
1125
|
+
try {
|
|
1126
|
+
const raw = localStorage.getItem(PLUGIN_REGISTRY_MEMORY_KEY);
|
|
1127
|
+
if (typeof raw !== 'string' || raw === '') return null;
|
|
1128
|
+
const parsed = JSON.parse(raw);
|
|
1129
|
+
if (parsed === null || typeof parsed !== 'object') return null;
|
|
1130
|
+
return parsed;
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* The registry choices the update dialog offers: the Host's configured
|
|
1138
|
+
* default (with the URL pnpm actually names), each configured fallback,
|
|
1139
|
+
* and a remembered custom address.
|
|
1140
|
+
* @param {{ registry: string | null, fallbackRegistries: string[], resolved: string | null } | null} registries
|
|
1141
|
+
* @param {object | null} remembered
|
|
1142
|
+
* @param {(key: string) => string} t
|
|
1143
|
+
* @returns {Array<{ key: string, label: string, hint: string, value: string | null, url: string }>}
|
|
1144
|
+
*/
|
|
1145
|
+
function buildRegistryChoices(registries, remembered, t) {
|
|
1146
|
+
const choices = [];
|
|
1147
|
+
const seen = new Set();
|
|
1148
|
+
const add = (choice) => {
|
|
1149
|
+
const key = choice.value === null ? 'default' : normalizeRegistry(choice.value);
|
|
1150
|
+
if (seen.has(key)) return;
|
|
1151
|
+
seen.add(key);
|
|
1152
|
+
choices.push({ ...choice, key });
|
|
1153
|
+
};
|
|
1154
|
+
const fallbacks = Array.isArray(registries?.fallbackRegistries) ? registries.fallbackRegistries : [];
|
|
1155
|
+
const resolved = typeof registries?.resolved === 'string' ? registries.resolved : OFFICIAL_REGISTRY;
|
|
1156
|
+
add({
|
|
1157
|
+
label: t('update.registryDefault'),
|
|
1158
|
+
hint: registryHost(resolved),
|
|
1159
|
+
value: registries?.registry ?? null,
|
|
1160
|
+
url: normalizeRegistry(registries?.registry ?? resolved),
|
|
1161
|
+
});
|
|
1162
|
+
for (const fallback of fallbacks) {
|
|
1163
|
+
if (typeof fallback !== 'string' || fallback.trim() === '') continue;
|
|
1164
|
+
const host = registryHost(fallback);
|
|
1165
|
+
add({
|
|
1166
|
+
label: REGISTRY_HOSTS.has(host) ? t(REGISTRY_HOSTS.get(host)) : host,
|
|
1167
|
+
hint: host,
|
|
1168
|
+
value: fallback,
|
|
1169
|
+
url: normalizeRegistry(fallback),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (remembered !== null && remembered.kind === 'custom' && typeof remembered.url === 'string' && remembered.url.trim() !== '') {
|
|
1173
|
+
add({
|
|
1174
|
+
label: t('update.registryCustom'),
|
|
1175
|
+
hint: registryHost(remembered.url),
|
|
1176
|
+
value: remembered.url.trim(),
|
|
1177
|
+
url: normalizeRegistry(remembered.url),
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const rememberedOffered = remembered !== null
|
|
1181
|
+
&& remembered.kind === 'offered'
|
|
1182
|
+
&& typeof remembered.registry === 'string'
|
|
1183
|
+
&& remembered.registry.trim() !== ''
|
|
1184
|
+
? remembered.registry
|
|
1185
|
+
: null;
|
|
1186
|
+
if (rememberedOffered !== null) {
|
|
1187
|
+
add({
|
|
1188
|
+
label: t('update.registryCustom'),
|
|
1189
|
+
hint: registryHost(rememberedOffered),
|
|
1190
|
+
value: rememberedOffered.trim(),
|
|
1191
|
+
url: normalizeRegistry(rememberedOffered),
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (choices.length === 0) {
|
|
1195
|
+
add({
|
|
1196
|
+
label: t('update.registryDefault'),
|
|
1197
|
+
hint: registryHost(OFFICIAL_REGISTRY),
|
|
1198
|
+
value: null,
|
|
1199
|
+
url: OFFICIAL_REGISTRY,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return choices;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Choose the offered registry matching what the Add dialog remembered.
|
|
1207
|
+
* @param {Array<{ key: string, value: string | null, url: string }>} choices
|
|
1208
|
+
* @param {object | null} remembered
|
|
1209
|
+
* @returns {object}
|
|
1210
|
+
*/
|
|
1211
|
+
function pickRegistryChoice(choices, remembered) {
|
|
1212
|
+
if (remembered !== null && typeof remembered === 'object') {
|
|
1213
|
+
if (remembered.kind === 'offered') {
|
|
1214
|
+
const wanted = remembered.registry === null ? null : normalizeRegistry(remembered.registry);
|
|
1215
|
+
const match = choices.find((choice) => (wanted === null
|
|
1216
|
+
? choice.value === null
|
|
1217
|
+
: choice.value !== null && normalizeRegistry(choice.value) === wanted));
|
|
1218
|
+
if (match !== undefined) return match;
|
|
1219
|
+
}
|
|
1220
|
+
if (remembered.kind === 'custom' && typeof remembered.url === 'string') {
|
|
1221
|
+
const wanted = normalizeRegistry(remembered.url);
|
|
1222
|
+
const match = choices.find((choice) => normalizeRegistry(choice.value ?? choice.url) === wanted);
|
|
1223
|
+
if (match !== undefined) return match;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return choices[0];
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** The lookup order: the chosen registry first, then the other offered URLs. */
|
|
1230
|
+
function lookupRegistries(choices, chosen) {
|
|
1231
|
+
const ordered = [];
|
|
1232
|
+
const seen = new Set();
|
|
1233
|
+
for (const choice of [chosen, ...choices]) {
|
|
1234
|
+
if (choice === null || choice === undefined) continue;
|
|
1235
|
+
const url = registryFetchUrl(choice);
|
|
1236
|
+
if (seen.has(url)) continue;
|
|
1237
|
+
seen.add(url);
|
|
1238
|
+
ordered.push(choice);
|
|
1239
|
+
}
|
|
1240
|
+
return ordered;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Split a version into its release and prerelease parts. */
|
|
1244
|
+
function versionParts(version) {
|
|
1245
|
+
const text = String(version ?? '').trim().replace(/^v/u, '');
|
|
1246
|
+
const match = /^(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(text);
|
|
1247
|
+
if (match === null) return null;
|
|
1248
|
+
return {
|
|
1249
|
+
release: match[1].split('.').map((part) => Number.parseInt(part, 10)),
|
|
1250
|
+
prerelease: match[2] === undefined ? null : match[2].split('.'),
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** Semver-ish descending comparator that keeps unparseable versions last. */
|
|
1255
|
+
function compareVersionsDesc(a, b) {
|
|
1256
|
+
const left = versionParts(a);
|
|
1257
|
+
const right = versionParts(b);
|
|
1258
|
+
if (left === null && right === null) return String(b).localeCompare(String(a));
|
|
1259
|
+
if (left === null) return 1;
|
|
1260
|
+
if (right === null) return -1;
|
|
1261
|
+
for (let index = 0; index < Math.max(left.release.length, right.release.length); index += 1) {
|
|
1262
|
+
const diff = (right.release[index] ?? 0) - (left.release[index] ?? 0);
|
|
1263
|
+
if (diff !== 0) return diff;
|
|
1264
|
+
}
|
|
1265
|
+
if (left.prerelease === null && right.prerelease === null) return 0;
|
|
1266
|
+
if (left.prerelease === null) return -1;
|
|
1267
|
+
if (right.prerelease === null) return 1;
|
|
1268
|
+
for (let index = 0; index < Math.max(left.prerelease.length, right.prerelease.length); index += 1) {
|
|
1269
|
+
const l = left.prerelease[index];
|
|
1270
|
+
const r = right.prerelease[index];
|
|
1271
|
+
if (l === undefined) return 1;
|
|
1272
|
+
if (r === undefined) return -1;
|
|
1273
|
+
const lNumber = /^\d+$/u.test(l);
|
|
1274
|
+
const rNumber = /^\d+$/u.test(r);
|
|
1275
|
+
if (lNumber && rNumber) {
|
|
1276
|
+
const diff = Number(r) - Number(l);
|
|
1277
|
+
if (diff !== 0) return diff;
|
|
1278
|
+
} else if (lNumber !== rNumber) {
|
|
1279
|
+
return lNumber ? 1 : -1;
|
|
1280
|
+
} else {
|
|
1281
|
+
const diff = r.localeCompare(l);
|
|
1282
|
+
if (diff !== 0) return diff;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return 0;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** Sorted, deduplicated version keys from registry metadata. */
|
|
1289
|
+
function sortVersions(versions) {
|
|
1290
|
+
const seen = new Set();
|
|
1291
|
+
const list = [];
|
|
1292
|
+
for (const version of versions ?? []) {
|
|
1293
|
+
if (typeof version !== 'string' || version === '' || seen.has(version)) continue;
|
|
1294
|
+
seen.add(version);
|
|
1295
|
+
list.push(version);
|
|
1296
|
+
}
|
|
1297
|
+
return list.sort(compareVersionsDesc);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Read the versions the registry publishes for one package. The
|
|
1302
|
+
* abbreviated metadata keeps the response small; both npm and npmmirror
|
|
1303
|
+
* send permissive CORS headers for this GET.
|
|
1304
|
+
* @param {string} registry
|
|
1305
|
+
* @param {string} name
|
|
1306
|
+
* @param {AbortSignal | undefined} signal
|
|
1307
|
+
* @returns {Promise<{ versions: string[], latest: string | null }>}
|
|
1308
|
+
*/
|
|
1309
|
+
async function fetchRegistryVersions(registry, name, signal) {
|
|
1310
|
+
const encoded = String(name).split('/').map((part) => encodeURIComponent(part).replace(/%40/gu, '@')).join('/');
|
|
1311
|
+
const url = `${normalizeRegistry(registry)}${encoded}`;
|
|
1312
|
+
let response = await fetch(url, {
|
|
1313
|
+
signal,
|
|
1314
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
1315
|
+
});
|
|
1316
|
+
if (!response.ok) {
|
|
1317
|
+
// Older or private registries may not serve the abbreviated document.
|
|
1318
|
+
response = await fetch(url, { signal });
|
|
1319
|
+
}
|
|
1320
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1321
|
+
const data = await response.json();
|
|
1322
|
+
const versions = sortVersions(Object.keys(data?.versions ?? {}));
|
|
1323
|
+
const latest = typeof data?.['dist-tags']?.latest === 'string' ? data['dist-tags'].latest : null;
|
|
1324
|
+
return { versions, latest };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Normalize a typed version. Only concrete versions are accepted: the
|
|
1329
|
+
* update removes the old copy before installing, so a typo must be
|
|
1330
|
+
* rejected while the plugin is still installed.
|
|
1331
|
+
* @param {unknown} value
|
|
1332
|
+
* @returns {string} the version without a leading `v`, or '' when invalid.
|
|
1333
|
+
*/
|
|
1334
|
+
function normalizeVersionInput(value) {
|
|
1335
|
+
const text = String(value ?? '').trim().replace(/^v/u, '');
|
|
1336
|
+
if (text === '' || /\s/u.test(text) || text.includes('@')) return '';
|
|
1337
|
+
return /^\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(text) ? text : '';
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** A display title for a bundle, following the page's locale fallback. */
|
|
1341
|
+
function packageDisplayTitle(pkg) {
|
|
1342
|
+
const title = pkg?.meta?.title;
|
|
1343
|
+
if (typeof title === 'string' && title !== '') return title;
|
|
1344
|
+
if (title !== null && typeof title === 'object') {
|
|
1345
|
+
const locale = String(navigator?.language ?? '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
1346
|
+
const localized = title[locale] ?? title.en;
|
|
1347
|
+
if (typeof localized === 'string' && localized !== '') return localized;
|
|
1348
|
+
}
|
|
1349
|
+
return String(pkg?.name ?? '');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/** The human sentence for a refused inspection. */
|
|
1353
|
+
function inspectReason(value, t) {
|
|
1354
|
+
if (value?.problem === 'already-installed') return t('update.alreadyInstalled');
|
|
1355
|
+
if (value?.problem === 'not-found') return t('update.notFound');
|
|
1356
|
+
if (value?.problem === 'not-a-bundle') return t('update.notBundle');
|
|
1357
|
+
return String(value?.reason ?? t('update.unknown'));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/** The diagnostic or localized sentence for a failed management operation. */
|
|
1361
|
+
function operationReason(value, t) {
|
|
1362
|
+
const code = value?.error?.code;
|
|
1363
|
+
if (code === 'management-required' || code === 'not-removable') return t('update.notRemovable');
|
|
1364
|
+
if (code === 'stop-profile') return t('update.stopProfile');
|
|
1365
|
+
if (code === 'not-bundle') return t('update.notBundle');
|
|
1366
|
+
if (code === 'already-installed') return t('update.alreadyInstalled');
|
|
1367
|
+
const diagnostic = value?.error?.diagnostic ?? value?.packageResult?.output ?? '';
|
|
1368
|
+
const text = String(diagnostic).trim();
|
|
1369
|
+
return text === '' ? t('update.unknown') : text;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/** A short error sentence. */
|
|
1373
|
+
function errorText(error) {
|
|
1374
|
+
return String(error?.message ?? error ?? '').trim();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/** A unique request id for the install stream. */
|
|
1378
|
+
function newRequestId() {
|
|
1379
|
+
try {
|
|
1380
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
1381
|
+
} catch {
|
|
1382
|
+
/* Fall through to the timestamp form. */
|
|
1383
|
+
}
|
|
1384
|
+
return `powerup-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/** The glyph on the injected card button (static; no external asset). */
|
|
1388
|
+
const UPDATE_BUTTON_ICON = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false"><path d="M8 10.6V3.1M5.2 5.9 8 3.1l2.8 2.8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.9 10.4v1A1.6 1.6 0 0 0 4.5 13h7a1.6 1.6 0 0 0 1.6-1.6v-1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>';
|
|
1389
|
+
|
|
1390
|
+
/** Tooltip/accessible label for one injected update button. */
|
|
1391
|
+
function updateButtonLabel(name, t) {
|
|
1392
|
+
return name === MODULE_ID ? t('update.selfTitle') : `${t('update.button')} · ${name}`;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Ensure every third-party card in the built-in "Installed plugins"
|
|
1397
|
+
* group carries one update button. Idempotent, so React re-renders and
|
|
1398
|
+
* the observer can call it repeatedly.
|
|
1399
|
+
* @param {Document} doc
|
|
1400
|
+
* @param {(name: string) => void} open
|
|
1401
|
+
* @param {(key: string, params?: object) => string} t
|
|
1402
|
+
* @param {Set<Element>} buttons
|
|
1403
|
+
*/
|
|
1404
|
+
function ensureUpdateButtons(doc, open, t, buttons) {
|
|
1405
|
+
if (doc === null || doc === undefined || typeof doc.querySelectorAll !== 'function') return;
|
|
1406
|
+
const cards = doc.querySelectorAll('[data-plugin-group="bundles"] [data-plugin-package]');
|
|
1407
|
+
for (const card of cards) {
|
|
1408
|
+
const name = card.getAttribute('data-plugin-package');
|
|
1409
|
+
if (typeof name !== 'string' || name === '') continue;
|
|
1410
|
+
let button = card.querySelector('[data-dsh-powerup-update]');
|
|
1411
|
+
if (button === null || button === undefined) {
|
|
1412
|
+
const head = card.firstElementChild;
|
|
1413
|
+
const end = head === null || head === undefined ? null : head.lastElementChild;
|
|
1414
|
+
if (end === null || end === undefined) continue;
|
|
1415
|
+
button = doc.createElement('button');
|
|
1416
|
+
button.type = 'button';
|
|
1417
|
+
button.className = 'dsh-powerup-update-button';
|
|
1418
|
+
button.setAttribute('data-dsh-powerup-update', '');
|
|
1419
|
+
button.innerHTML = UPDATE_BUTTON_ICON;
|
|
1420
|
+
button.addEventListener('click', (event) => {
|
|
1421
|
+
event.preventDefault();
|
|
1422
|
+
event.stopPropagation();
|
|
1423
|
+
open(name);
|
|
1424
|
+
});
|
|
1425
|
+
end.insertBefore(button, end.firstChild);
|
|
1426
|
+
buttons.add(button);
|
|
1427
|
+
}
|
|
1428
|
+
const label = updateButtonLabel(name, t);
|
|
1429
|
+
if (button.title !== label) button.title = label;
|
|
1430
|
+
if (button.getAttribute('aria-label') !== label) button.setAttribute('aria-label', label);
|
|
1431
|
+
}
|
|
1432
|
+
for (const button of [...buttons]) if (button.isConnected !== true) buttons.delete(button);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* The Plugins-page update control. It injects one icon button into each
|
|
1437
|
+
* third-party card of the built-in "Installed plugins" group, then owns
|
|
1438
|
+
* the dialog that removes the old version, installs the selected one
|
|
1439
|
+
* through the same `pluginManager` Remote the Add plugin dialog uses,
|
|
1440
|
+
* asks about enabling, and offers to restart DSH.
|
|
1441
|
+
*/
|
|
1442
|
+
function PluginUpdateOverlay({ manager, remote, ctx }) {
|
|
1443
|
+
const [view, setView] = useState(null);
|
|
1444
|
+
const [localeTick, forceLocale] = useState(0);
|
|
1445
|
+
const tokenRef = useRef(null);
|
|
1446
|
+
const abortRef = useRef(null);
|
|
1447
|
+
const buttonsRef = useRef(new Set());
|
|
1448
|
+
const inputRef = useRef(null);
|
|
1449
|
+
const versionSeq = useRef(0);
|
|
1450
|
+
const tRef = useRef((key, params) => translateCurrent(key, params));
|
|
1451
|
+
tRef.current = (key, params) => translateCurrent(key, params);
|
|
1452
|
+
|
|
1453
|
+
useEffect(() => {
|
|
1454
|
+
const locale = ctx?.get?.('locale');
|
|
1455
|
+
if (locale === undefined || typeof locale.subscribe !== 'function') return undefined;
|
|
1456
|
+
return locale.subscribe(() => forceLocale((value) => value + 1));
|
|
1457
|
+
}, []);
|
|
1458
|
+
|
|
1459
|
+
/** Merge a patch into the open dialog only while its token is current. */
|
|
1460
|
+
const patch = useCallback((token, next) => {
|
|
1461
|
+
setView((current) => {
|
|
1462
|
+
if (current === null || tokenRef.current !== token) return current;
|
|
1463
|
+
return { ...current, ...(typeof next === 'function' ? next(current) : next) };
|
|
1464
|
+
});
|
|
1465
|
+
}, []);
|
|
1466
|
+
|
|
1467
|
+
const close = useCallback(() => {
|
|
1468
|
+
const current = view;
|
|
1469
|
+
if (current !== null && (current.phase === 'working' || current.phase === 'restarting' || current.enabling === true)) return;
|
|
1470
|
+
tokenRef.current = null;
|
|
1471
|
+
try {
|
|
1472
|
+
abortRef.current?.abort?.();
|
|
1473
|
+
} catch {
|
|
1474
|
+
/* ignore */
|
|
1475
|
+
}
|
|
1476
|
+
abortRef.current = null;
|
|
1477
|
+
setView(null);
|
|
1478
|
+
}, [view]);
|
|
1479
|
+
|
|
1480
|
+
const loadVersions = useCallback(async (token, name, choice, choices, resetVersion) => {
|
|
1481
|
+
const t = tRef.current;
|
|
1482
|
+
const seq = versionSeq.current + 1;
|
|
1483
|
+
versionSeq.current = seq;
|
|
1484
|
+
const current = () => versionSeq.current === seq && tokenRef.current === token;
|
|
1485
|
+
patch(token, { versionsState: 'loading', versionsError: null, versionsRegistry: null, ...(resetVersion === true ? { versions: [], version: '' } : {}) });
|
|
1486
|
+
let lastError = null;
|
|
1487
|
+
for (const candidate of lookupRegistries(choices, choice)) {
|
|
1488
|
+
if (!current()) return;
|
|
1489
|
+
const lifetime = abortRef.current?.signal;
|
|
1490
|
+
const timeout = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1491
|
+
? AbortSignal.timeout(15000)
|
|
1492
|
+
: null;
|
|
1493
|
+
const fetchSignal = lifetime !== undefined && lifetime !== null && timeout !== null && typeof AbortSignal.any === 'function'
|
|
1494
|
+
? AbortSignal.any([lifetime, timeout])
|
|
1495
|
+
: (lifetime ?? timeout ?? undefined);
|
|
1496
|
+
try {
|
|
1497
|
+
const result = await fetchRegistryVersions(registryFetchUrl(candidate), name, fetchSignal);
|
|
1498
|
+
if (!current()) return;
|
|
1499
|
+
if (result.versions.length === 0) {
|
|
1500
|
+
lastError = new Error(t('update.versionsEmpty'));
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
patch(token, { versions: result.versions, versionsState: 'ready', versionsError: null, versionsRegistry: candidate, latest: result.latest });
|
|
1504
|
+
return;
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (!current()) return;
|
|
1507
|
+
lastError = error;
|
|
1508
|
+
if (abortRef.current?.signal?.aborted === true) return;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
patch(token, { versionsState: 'error', versionsError: errorText(lastError) || t('update.unknown') });
|
|
1512
|
+
}, [patch]);
|
|
1513
|
+
|
|
1514
|
+
const openFor = useCallback(async (name) => {
|
|
1515
|
+
const t = tRef.current;
|
|
1516
|
+
const token = {};
|
|
1517
|
+
tokenRef.current = token;
|
|
1518
|
+
try {
|
|
1519
|
+
abortRef.current?.abort?.();
|
|
1520
|
+
} catch {
|
|
1521
|
+
/* ignore */
|
|
1522
|
+
}
|
|
1523
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1524
|
+
abortRef.current = controller;
|
|
1525
|
+
setView({
|
|
1526
|
+
name,
|
|
1527
|
+
title: name,
|
|
1528
|
+
currentVersion: null,
|
|
1529
|
+
phase: 'loading',
|
|
1530
|
+
step: null,
|
|
1531
|
+
choices: [],
|
|
1532
|
+
registryKey: '',
|
|
1533
|
+
versions: [],
|
|
1534
|
+
versionsState: 'loading',
|
|
1535
|
+
versionsError: null,
|
|
1536
|
+
versionsRegistry: null,
|
|
1537
|
+
version: '',
|
|
1538
|
+
listOpen: true,
|
|
1539
|
+
error: null,
|
|
1540
|
+
formError: null,
|
|
1541
|
+
log: '',
|
|
1542
|
+
enabling: false,
|
|
1543
|
+
restarting: false,
|
|
1544
|
+
});
|
|
1545
|
+
if (name === MODULE_ID) {
|
|
1546
|
+
patch(token, { phase: 'self' });
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const [bundles, registries] = await Promise.all([manager.listBundles(), manager.registries()]);
|
|
1551
|
+
if (tokenRef.current !== token) return;
|
|
1552
|
+
if (!bundles.ok) throw new Error(bundles.error?.message ?? t('update.unknown'));
|
|
1553
|
+
const pkg = (bundles.value ?? []).find((item) => item.name === name);
|
|
1554
|
+
if (pkg === undefined) throw new Error(t('update.unknown'));
|
|
1555
|
+
if (pkg.readOnlyReason !== undefined || pkg.removable === false) {
|
|
1556
|
+
patch(token, {
|
|
1557
|
+
title: packageDisplayTitle(pkg),
|
|
1558
|
+
currentVersion: pkg.version ?? null,
|
|
1559
|
+
phase: 'blocked',
|
|
1560
|
+
error: pkg.readOnlyReason === 'management-required' ? t('update.notRemovable') : null,
|
|
1561
|
+
});
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const remembered = readRememberedRegistry();
|
|
1565
|
+
const choices = buildRegistryChoices(registries.ok ? registries.value : null, remembered, t);
|
|
1566
|
+
const choice = pickRegistryChoice(choices, remembered);
|
|
1567
|
+
patch(token, {
|
|
1568
|
+
title: packageDisplayTitle(pkg),
|
|
1569
|
+
currentVersion: pkg.version ?? null,
|
|
1570
|
+
installed: pkg.installed === true,
|
|
1571
|
+
phase: 'choose',
|
|
1572
|
+
choices,
|
|
1573
|
+
registryKey: choice.key,
|
|
1574
|
+
versionsState: 'loading',
|
|
1575
|
+
});
|
|
1576
|
+
await loadVersions(token, name, choice, choices, false);
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
patch(token, { phase: 'error', error: errorText(error) || t('update.unknown') });
|
|
1579
|
+
}
|
|
1580
|
+
}, [loadVersions, manager, patch]);
|
|
1581
|
+
|
|
1582
|
+
// Inject an update button into every third-party installed card. The
|
|
1583
|
+
// built-in page owns these nodes, so re-assert after React renders and
|
|
1584
|
+
// remove our own buttons on unload. Open through a ref to keep the
|
|
1585
|
+
// observer alive across state changes.
|
|
1586
|
+
const openRef = useRef(openFor);
|
|
1587
|
+
openRef.current = openFor;
|
|
1588
|
+
useEffect(() => {
|
|
1589
|
+
const buttons = buttonsRef.current;
|
|
1590
|
+
let scheduled = false;
|
|
1591
|
+
let stopped = false;
|
|
1592
|
+
const ensure = () => {
|
|
1593
|
+
scheduled = false;
|
|
1594
|
+
if (stopped) return;
|
|
1595
|
+
ensureUpdateButtons(document, (name) => openRef.current(name), tRef.current, buttons);
|
|
1596
|
+
};
|
|
1597
|
+
const schedule = () => {
|
|
1598
|
+
if (scheduled || stopped) return;
|
|
1599
|
+
scheduled = true;
|
|
1600
|
+
if (typeof window.requestAnimationFrame === 'function') window.requestAnimationFrame(ensure);
|
|
1601
|
+
else window.setTimeout(ensure, 16);
|
|
1602
|
+
};
|
|
1603
|
+
ensure();
|
|
1604
|
+
const observer = new MutationObserver(schedule);
|
|
1605
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1606
|
+
return () => {
|
|
1607
|
+
stopped = true;
|
|
1608
|
+
observer.disconnect();
|
|
1609
|
+
for (const button of buttons) {
|
|
1610
|
+
try {
|
|
1611
|
+
button.remove();
|
|
1612
|
+
} catch {
|
|
1613
|
+
/* ignore */
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
buttons.clear();
|
|
1617
|
+
};
|
|
1618
|
+
}, [localeTick]);
|
|
1619
|
+
|
|
1620
|
+
// Keep the selected version input focused when the chooser opens.
|
|
1621
|
+
useEffect(() => {
|
|
1622
|
+
if (view !== null && view.phase === 'choose' && inputRef.current !== null) {
|
|
1623
|
+
try {
|
|
1624
|
+
inputRef.current.focus();
|
|
1625
|
+
} catch {
|
|
1626
|
+
/* focus is best-effort */
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}, [view === null ? null : view.phase]);
|
|
1630
|
+
|
|
1631
|
+
// Escape closes a dialog that is not in the middle of an operation.
|
|
1632
|
+
useEffect(() => {
|
|
1633
|
+
if (view === null) return undefined;
|
|
1634
|
+
const onKeyDown = (event) => {
|
|
1635
|
+
if (event.key !== 'Escape') return;
|
|
1636
|
+
event.stopPropagation();
|
|
1637
|
+
close();
|
|
1638
|
+
};
|
|
1639
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
1640
|
+
return () => document.removeEventListener('keydown', onKeyDown, true);
|
|
1641
|
+
}, [view, close]);
|
|
1642
|
+
|
|
1643
|
+
// Show pnpm output while an installation runs.
|
|
1644
|
+
useEffect(() => {
|
|
1645
|
+
if (remote === undefined || remote === null || typeof remote.$on !== 'function') return undefined;
|
|
1646
|
+
const offLog = remote.$on('plugin-manager/install-log', (chunk) => {
|
|
1647
|
+
if (chunk === null || typeof chunk !== 'object') return;
|
|
1648
|
+
const token = tokenRef.current;
|
|
1649
|
+
const text = typeof chunk.text === 'string' ? chunk.text : '';
|
|
1650
|
+
if (token === null || text === '') return;
|
|
1651
|
+
setView((current) => current === null || current.phase !== 'working'
|
|
1652
|
+
? current
|
|
1653
|
+
: { ...current, log: (current.log + text).slice(-2400) });
|
|
1654
|
+
});
|
|
1655
|
+
return () => {
|
|
1656
|
+
try {
|
|
1657
|
+
offLog?.();
|
|
1658
|
+
} catch {
|
|
1659
|
+
/* ignore */
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}, [remote]);
|
|
1663
|
+
|
|
1664
|
+
const chooseVersion = (version) => {
|
|
1665
|
+
if (tokenRef.current === null) return;
|
|
1666
|
+
setView((current) => current === null ? current : { ...current, version, listOpen: false, formError: null });
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
const chooseRegistry = (key) => {
|
|
1670
|
+
const token = tokenRef.current;
|
|
1671
|
+
if (token === null) return;
|
|
1672
|
+
const current = view;
|
|
1673
|
+
if (current === null) return;
|
|
1674
|
+
const choice = current.choices.find((item) => item.key === key);
|
|
1675
|
+
if (choice === undefined) return;
|
|
1676
|
+
setView((state) => state === null ? state : { ...state, registryKey: key, formError: null });
|
|
1677
|
+
loadVersions(token, current.name, choice, current.choices, true);
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
const startUpdate = async () => {
|
|
1681
|
+
const token = tokenRef.current;
|
|
1682
|
+
const current = view;
|
|
1683
|
+
if (token === null || current === null || current.phase !== 'choose') return;
|
|
1684
|
+
const t = tRef.current;
|
|
1685
|
+
const version = normalizeVersionInput(current.version);
|
|
1686
|
+
if (version === '') {
|
|
1687
|
+
setView((state) => state === null ? state : { ...state, formError: t('update.invalidVersion') });
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const choice = current.choices.find((item) => item.key === current.registryKey) ?? current.choices[0];
|
|
1691
|
+
const registry = registryValue(choice);
|
|
1692
|
+
const spec = `${current.name}@${version}`;
|
|
1693
|
+
const requestId = newRequestId();
|
|
1694
|
+
patch(token, { phase: 'working', step: 'removing', error: null, formError: null, log: '', logLabel: spec });
|
|
1695
|
+
try {
|
|
1696
|
+
// The update flow removes the old version first: the Host's
|
|
1697
|
+
// inspection refuses a package that is still installed, so the
|
|
1698
|
+
// version is checked after removal, exactly where Add plugin
|
|
1699
|
+
// performs its own check.
|
|
1700
|
+
const removed = await manager.removeBundle(current.name);
|
|
1701
|
+
if (tokenRef.current !== token) return;
|
|
1702
|
+
if (!removed.ok) throw new Error(removed.error?.message ?? t('update.unknown'));
|
|
1703
|
+
if (removed.value.error !== undefined) throw new Error(operationReason(removed.value, t));
|
|
1704
|
+
patch(token, { step: 'checking' });
|
|
1705
|
+
const checked = await manager.inspect(spec, { registry });
|
|
1706
|
+
if (tokenRef.current !== token) return;
|
|
1707
|
+
if (!checked.ok) throw new Error(checked.error?.message ?? t('update.unknown'));
|
|
1708
|
+
if (checked.value.status !== 'accepted') throw new Error(inspectReason(checked.value, t));
|
|
1709
|
+
if (checked.value.bundle === false) throw new Error(t('update.notBundle'));
|
|
1710
|
+
patch(token, { step: 'installing' });
|
|
1711
|
+
const installed = await manager.installBundle(spec, { enabled: false, requestId, registry });
|
|
1712
|
+
if (tokenRef.current !== token) return;
|
|
1713
|
+
if (!installed.ok) throw new Error(installed.error?.message ?? t('update.unknown'));
|
|
1714
|
+
const value = installed.value;
|
|
1715
|
+
if (value.error !== undefined || value.application === 'failed') throw new Error(operationReason(value, t));
|
|
1716
|
+
if (value.application === 'cancelled') throw new Error(t('update.cancelled'));
|
|
1717
|
+
patch(token, {
|
|
1718
|
+
phase: 'enable',
|
|
1719
|
+
step: null,
|
|
1720
|
+
installedVersion: value.version ?? version,
|
|
1721
|
+
restartRequired: value.application === 'restart-required',
|
|
1722
|
+
enabling: false,
|
|
1723
|
+
});
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
patch(token, { phase: 'error', step: null, error: errorText(error) || t('update.unknown') });
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
/** Ask the Host route whether this runtime can restart itself. */
|
|
1730
|
+
const readRestartCapability = async () => {
|
|
1731
|
+
try {
|
|
1732
|
+
const response = await fetch(UPDATE_API, { headers: { accept: 'application/json' } });
|
|
1733
|
+
const data = await response.json();
|
|
1734
|
+
return {
|
|
1735
|
+
supported: response.ok && data?.ok === true && data.supported === true,
|
|
1736
|
+
reason: typeof data?.reason === 'string' ? data.reason : '',
|
|
1737
|
+
};
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
return { supported: false, reason: errorText(error) };
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
const enableNow = async () => {
|
|
1744
|
+
const token = tokenRef.current;
|
|
1745
|
+
const current = view;
|
|
1746
|
+
if (token === null || current === null) return;
|
|
1747
|
+
const t = tRef.current;
|
|
1748
|
+
patch(token, { enabling: true, enableError: null });
|
|
1749
|
+
try {
|
|
1750
|
+
const result = await manager.setBundleEnabled(current.name, true);
|
|
1751
|
+
if (tokenRef.current !== token) return;
|
|
1752
|
+
if (!result.ok) throw new Error(result.error?.message ?? t('update.unknown'));
|
|
1753
|
+
if (result.value.error !== undefined) throw new Error(operationReason(result.value, t));
|
|
1754
|
+
const restart = await readRestartCapability();
|
|
1755
|
+
patch(token, { phase: 'restart', enabling: false, restart });
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
patch(token, { enabling: false, enableError: errorText(error) || t('update.unknown') });
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
const enableLater = () => {
|
|
1762
|
+
const token = tokenRef.current;
|
|
1763
|
+
if (token === null) return;
|
|
1764
|
+
patch(token, { phase: 'done', enabled: false });
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
const skipRestart = () => {
|
|
1768
|
+
const token = tokenRef.current;
|
|
1769
|
+
if (token === null) return;
|
|
1770
|
+
patch(token, { phase: 'done', restartSkipped: true });
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
const restartNow = async () => {
|
|
1774
|
+
const token = tokenRef.current;
|
|
1775
|
+
if (token === null) return;
|
|
1776
|
+
const t = tRef.current;
|
|
1777
|
+
patch(token, { restarting: true, restartError: null });
|
|
1778
|
+
try {
|
|
1779
|
+
const response = await fetch(UPDATE_API, {
|
|
1780
|
+
method: 'POST',
|
|
1781
|
+
headers: { 'content-type': 'application/json' },
|
|
1782
|
+
body: JSON.stringify({ action: 'restart' }),
|
|
1783
|
+
});
|
|
1784
|
+
const data = await response.json().catch(() => null);
|
|
1785
|
+
if (!response.ok || data?.ok !== true) throw new Error(data?.error ?? `HTTP ${response.status}`);
|
|
1786
|
+
patch(token, { phase: 'restarting', restarting: true });
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
patch(token, { restarting: false, restartError: errorText(error) || t('update.unknown') });
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
if (view === null) return null;
|
|
1793
|
+
const t = (key, params) => translateCurrent(key, params);
|
|
1794
|
+
const busy = view.phase === 'working' || view.phase === 'restarting' || view.enabling === true;
|
|
1795
|
+
const canClose = !busy;
|
|
1796
|
+
const title = view.title === undefined || view.title === '' ? view.name : view.title;
|
|
1797
|
+
const showVersions = view.phase === 'choose' && view.listOpen === true;
|
|
1798
|
+
|
|
1799
|
+
const footer = (() => {
|
|
1800
|
+
if (view.phase === 'choose') {
|
|
1801
|
+
return [
|
|
1802
|
+
h('button', { key: 'cancel', type: 'button', className: 'dsh-powerup-update-ghost', onClick: close }, t('update.cancel')),
|
|
1803
|
+
h('button', {
|
|
1804
|
+
key: 'start',
|
|
1805
|
+
type: 'button',
|
|
1806
|
+
className: 'dsh-powerup-update-primary',
|
|
1807
|
+
disabled: view.versionsState === 'loading' || String(view.version ?? '').trim() === '',
|
|
1808
|
+
onClick: startUpdate,
|
|
1809
|
+
}, t('update.start')),
|
|
1810
|
+
];
|
|
1811
|
+
}
|
|
1812
|
+
if (view.phase === 'enable') {
|
|
1813
|
+
return [
|
|
1814
|
+
h('button', { key: 'later', type: 'button', className: 'dsh-powerup-update-ghost', disabled: view.enabling === true, onClick: enableLater }, t('update.enableLater')),
|
|
1815
|
+
h('button', { key: 'now', type: 'button', className: 'dsh-powerup-update-primary', disabled: view.enabling === true, onClick: enableNow }, view.enabling === true ? t('update.step.enabling') : t('update.enableNow')),
|
|
1816
|
+
];
|
|
1817
|
+
}
|
|
1818
|
+
if (view.phase === 'restart') {
|
|
1819
|
+
const supported = view.restart?.supported === true;
|
|
1820
|
+
return [
|
|
1821
|
+
h('button', { key: 'later', type: 'button', className: 'dsh-powerup-update-ghost', disabled: view.restarting === true, onClick: skipRestart }, supported ? t('update.restartLater') : t('update.close')),
|
|
1822
|
+
supported ? h('button', { key: 'now', type: 'button', className: 'dsh-powerup-update-primary', disabled: view.restarting === true, onClick: restartNow }, view.restarting === true ? t('update.restarting') : t('update.restartNow')) : null,
|
|
1823
|
+
].filter(Boolean);
|
|
1824
|
+
}
|
|
1825
|
+
if (view.phase === 'restarting') return [];
|
|
1826
|
+
return [
|
|
1827
|
+
h('button', { key: 'done', type: 'button', className: 'dsh-powerup-update-primary', onClick: close }, view.phase === 'done' ? t('update.doneButton') : t('update.close')),
|
|
1828
|
+
];
|
|
1829
|
+
})();
|
|
1830
|
+
|
|
1831
|
+
const body = (() => {
|
|
1832
|
+
if (view.phase === 'loading') {
|
|
1833
|
+
return h('div', { className: 'dsh-powerup-update-status' },
|
|
1834
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
1835
|
+
t('update.loading'),
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
if (view.phase === 'self') {
|
|
1839
|
+
return [
|
|
1840
|
+
h('p', { className: 'dsh-powerup-update-note' }, t('update.selfDesc')),
|
|
1841
|
+
];
|
|
1842
|
+
}
|
|
1843
|
+
if (view.phase === 'blocked') {
|
|
1844
|
+
return h('div', { className: 'dsh-powerup-update-error' },
|
|
1845
|
+
h(WarnIcon, { size: 15 }),
|
|
1846
|
+
h('span', null, view.error ?? t('update.notRemovable')),
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
if (view.phase === 'choose') {
|
|
1850
|
+
return [
|
|
1851
|
+
h('div', { key: 'name', className: 'dsh-powerup-update-name' }, view.name),
|
|
1852
|
+
h('div', { key: 'meta', className: 'dsh-powerup-update-meta' },
|
|
1853
|
+
h('span', null, `${t('update.current')} ${view.currentVersion ?? '—'}`),
|
|
1854
|
+
view.installed === false ? h('span', { className: 'dsh-powerup-update-chip' }, t('update.installedChip')) : null,
|
|
1855
|
+
),
|
|
1856
|
+
h('div', { key: 'version', className: 'dsh-powerup-update-field' },
|
|
1857
|
+
h('label', { className: 'dsh-powerup-update-label', htmlFor: 'dsh-powerup-update-version' }, t('update.versionLabel')),
|
|
1858
|
+
h('div', { className: 'dsh-powerup-update-row' },
|
|
1859
|
+
h('input', {
|
|
1860
|
+
id: 'dsh-powerup-update-version',
|
|
1861
|
+
ref: inputRef,
|
|
1862
|
+
className: 'dsh-powerup-update-input',
|
|
1863
|
+
type: 'text',
|
|
1864
|
+
value: view.version ?? '',
|
|
1865
|
+
placeholder: t('update.versionPlaceholder'),
|
|
1866
|
+
autoComplete: 'off',
|
|
1867
|
+
spellCheck: false,
|
|
1868
|
+
onChange: (event) => setView((current) => current === null ? current : { ...current, version: event.target.value, formError: null }),
|
|
1869
|
+
onKeyDown: (event) => {
|
|
1870
|
+
if (event.key === 'Enter') startUpdate();
|
|
1871
|
+
},
|
|
1872
|
+
}),
|
|
1873
|
+
h('button', {
|
|
1874
|
+
type: 'button',
|
|
1875
|
+
className: 'dsh-powerup-update-toggle',
|
|
1876
|
+
'aria-expanded': showVersions ? 'true' : 'false',
|
|
1877
|
+
onClick: () => setView((current) => current === null ? current : { ...current, listOpen: !(current.listOpen === true) }),
|
|
1878
|
+
}, t('update.chooseVersion')),
|
|
1879
|
+
),
|
|
1880
|
+
view.formError === null || view.formError === undefined ? null : h('p', { className: 'dsh-powerup-update-error' }, h(WarnIcon, { size: 14 }), h('span', null, view.formError)),
|
|
1881
|
+
),
|
|
1882
|
+
showVersions ? h('div', { key: 'versions', className: 'dsh-powerup-update-list' }, (() => {
|
|
1883
|
+
if (view.versionsState === 'loading') {
|
|
1884
|
+
return h('div', { className: 'dsh-powerup-update-status' },
|
|
1885
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
1886
|
+
t('update.loadingVersions'),
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
if (view.versionsState === 'error') {
|
|
1890
|
+
return h('div', { className: 'dsh-powerup-update-version' },
|
|
1891
|
+
h('span', null, t('update.versionsFailed', { message: view.versionsError ?? '' })),
|
|
1892
|
+
h('button', {
|
|
1893
|
+
type: 'button',
|
|
1894
|
+
className: 'dsh-powerup-update-ghost',
|
|
1895
|
+
onClick: () => loadVersions(tokenRef.current, view.name, view.choices.find((item) => item.key === view.registryKey) ?? view.choices[0], view.choices, false),
|
|
1896
|
+
}, t('update.retry')),
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
if ((view.versions ?? []).length === 0) {
|
|
1900
|
+
return h('div', { className: 'dsh-powerup-update-version' }, t('update.versionsEmpty'));
|
|
1901
|
+
}
|
|
1902
|
+
return view.versions.map((version) => h('button', {
|
|
1903
|
+
key: version,
|
|
1904
|
+
type: 'button',
|
|
1905
|
+
className: 'dsh-powerup-update-version',
|
|
1906
|
+
'aria-selected': version === view.version ? 'true' : 'false',
|
|
1907
|
+
onClick: () => chooseVersion(version),
|
|
1908
|
+
},
|
|
1909
|
+
h('span', null, version),
|
|
1910
|
+
version === view.latest ? h('span', { className: 'dsh-powerup-update-tag' }, t('update.latest')) : null,
|
|
1911
|
+
));
|
|
1912
|
+
})()) : null,
|
|
1913
|
+
h('div', { key: 'registry', className: 'dsh-powerup-update-registry' },
|
|
1914
|
+
h('span', { className: 'dsh-powerup-update-label' }, t('update.registry')),
|
|
1915
|
+
h('select', {
|
|
1916
|
+
className: 'dsh-powerup-update-select',
|
|
1917
|
+
value: view.registryKey,
|
|
1918
|
+
onChange: (event) => chooseRegistry(event.target.value),
|
|
1919
|
+
}, view.choices.map((choice) => h('option', { key: choice.key, value: choice.key }, `${choice.label} · ${choice.hint}`))),
|
|
1920
|
+
view.versionsRegistry === null || view.versionsRegistry === undefined
|
|
1921
|
+
? null
|
|
1922
|
+
: h('span', { className: 'dsh-powerup-update-note' }, t('update.registryFrom', { registry: view.versionsRegistry.hint })),
|
|
1923
|
+
),
|
|
1924
|
+
];
|
|
1925
|
+
}
|
|
1926
|
+
if (view.phase === 'working') {
|
|
1927
|
+
const label = view.step === 'removing'
|
|
1928
|
+
? t('update.step.removing')
|
|
1929
|
+
: view.step === 'installing'
|
|
1930
|
+
? t('update.step.installing', { version: view.logLabel ?? '' })
|
|
1931
|
+
: t('update.step.checking');
|
|
1932
|
+
return [
|
|
1933
|
+
h('div', { key: 'status', className: 'dsh-powerup-update-status' },
|
|
1934
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
1935
|
+
label,
|
|
1936
|
+
),
|
|
1937
|
+
h('div', { key: 'name', className: 'dsh-powerup-update-name' }, view.name),
|
|
1938
|
+
view.log === '' ? null : h('div', { key: 'log', className: 'dsh-powerup-update-field' },
|
|
1939
|
+
h('span', { className: 'dsh-powerup-update-label' }, t('update.log')),
|
|
1940
|
+
h('pre', { className: 'dsh-powerup-update-log' }, view.log),
|
|
1941
|
+
),
|
|
1942
|
+
];
|
|
1943
|
+
}
|
|
1944
|
+
if (view.phase === 'enable') {
|
|
1945
|
+
return [
|
|
1946
|
+
h('div', { key: 'title', className: 'dsh-powerup-update-meta' },
|
|
1947
|
+
h(CheckIcon, { size: 15 }),
|
|
1948
|
+
h('span', null, t('update.enableTitle')),
|
|
1949
|
+
),
|
|
1950
|
+
h('p', { key: 'desc', className: 'dsh-powerup-update-note' }, t('update.enableDesc')),
|
|
1951
|
+
h('div', { key: 'name', className: 'dsh-powerup-update-name' }, `${view.name} · ${view.installedVersion ?? ''}`),
|
|
1952
|
+
view.enableError === null || view.enableError === undefined ? null : h('div', { key: 'error', className: 'dsh-powerup-update-error' }, h(WarnIcon, { size: 14 }), h('span', null, t('update.enableFailed', { message: view.enableError }))),
|
|
1953
|
+
];
|
|
1954
|
+
}
|
|
1955
|
+
if (view.phase === 'restart') {
|
|
1956
|
+
const supported = view.restart?.supported === true;
|
|
1957
|
+
return [
|
|
1958
|
+
h('div', { key: 'title', className: 'dsh-powerup-update-meta' },
|
|
1959
|
+
h(CheckIcon, { size: 15 }),
|
|
1960
|
+
h('span', null, t('update.restartTitle')),
|
|
1961
|
+
),
|
|
1962
|
+
h('p', { key: 'desc', className: 'dsh-powerup-update-note' }, t('update.restartDesc')),
|
|
1963
|
+
h('div', { key: 'name', className: 'dsh-powerup-update-name' }, `${view.name} · ${view.installedVersion ?? ''}`),
|
|
1964
|
+
supported ? null : h('p', { key: 'unsupported', className: 'dsh-powerup-update-note' }, t('update.restartUnsupported', { reason: view.restart?.reason ?? '' })),
|
|
1965
|
+
view.restartError === null || view.restartError === undefined ? null : h('div', { key: 'error', className: 'dsh-powerup-update-error' }, h(WarnIcon, { size: 14 }), h('span', null, t('update.restartFailed', { message: view.restartError }))),
|
|
1966
|
+
];
|
|
1967
|
+
}
|
|
1968
|
+
if (view.phase === 'restarting') {
|
|
1969
|
+
return h('div', { className: 'dsh-powerup-update-status' },
|
|
1970
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
1971
|
+
t('update.restarting'),
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
if (view.phase === 'done') {
|
|
1975
|
+
return [
|
|
1976
|
+
h('div', { key: 'title', className: 'dsh-powerup-update-meta' }, h(CheckIcon, { size: 15 }), h('span', null, t('update.doneTitle'))),
|
|
1977
|
+
h('p', { key: 'desc', className: 'dsh-powerup-update-note' }, view.enabled === false
|
|
1978
|
+
? t('update.doneDisabled', { name: view.name, version: view.installedVersion ?? '' })
|
|
1979
|
+
: t('update.doneInstalled', { name: view.name, version: view.installedVersion ?? '' })),
|
|
1980
|
+
view.restartSkipped === true ? h('p', { key: 'restart', className: 'dsh-powerup-update-note' }, t('update.doneRestart')) : null,
|
|
1981
|
+
];
|
|
1982
|
+
}
|
|
1983
|
+
if (view.phase === 'error') {
|
|
1984
|
+
return [
|
|
1985
|
+
h('div', { key: 'title', className: 'dsh-powerup-update-error' }, h(WarnIcon, { size: 15 }), h('span', null, t('update.errorTitle'))),
|
|
1986
|
+
h('p', { key: 'message', className: 'dsh-powerup-update-note' }, view.error ?? t('update.unknown')),
|
|
1987
|
+
h('div', { key: 'name', className: 'dsh-powerup-update-name' }, view.name),
|
|
1988
|
+
];
|
|
1989
|
+
}
|
|
1990
|
+
return null;
|
|
1991
|
+
})();
|
|
1992
|
+
|
|
1993
|
+
return h('div', {
|
|
1994
|
+
className: 'dsh-powerup-update-mask',
|
|
1995
|
+
onMouseDown: (event) => {
|
|
1996
|
+
if (event.target === event.currentTarget) close();
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
h('div', {
|
|
2000
|
+
className: 'dsh-powerup-update-dialog',
|
|
2001
|
+
role: 'dialog',
|
|
2002
|
+
'aria-modal': 'true',
|
|
2003
|
+
'aria-labelledby': 'dsh-powerup-update-title',
|
|
2004
|
+
},
|
|
2005
|
+
h('div', { className: 'dsh-powerup-update-head' },
|
|
2006
|
+
h('span', { className: 'dsh-powerup-update-icon' }, h(UpdateIcon, { size: 15 })),
|
|
2007
|
+
h('div', { className: 'dsh-powerup-update-heading' },
|
|
2008
|
+
h('h2', { id: 'dsh-powerup-update-title' }, view.phase === 'self' ? t('update.selfTitle') : t('update.title')),
|
|
2009
|
+
h('p', { title }, title),
|
|
2010
|
+
),
|
|
2011
|
+
canClose ? h('button', { type: 'button', className: 'dsh-powerup-update-close', 'aria-label': t('update.close'), onClick: close }, '×') : null,
|
|
2012
|
+
),
|
|
2013
|
+
h('div', { className: 'dsh-powerup-update-body' }, body),
|
|
2014
|
+
footer.length === 0 ? null : h('div', { className: 'dsh-powerup-update-foot' }, footer),
|
|
2015
|
+
),
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function buildRowItems(row, snapshot, t, sessionId, ctx, notify) {
|
|
2020
|
+
const text = rowText(row);
|
|
2021
|
+
const forkSeq = resolveForkSeq(row, snapshot);
|
|
2022
|
+
const quote = markdownQuote(row, t);
|
|
2023
|
+
const hasText = text !== '';
|
|
2024
|
+
const hasSession = typeof sessionId === 'string' && sessionId !== '';
|
|
2025
|
+
return [
|
|
2026
|
+
{
|
|
2027
|
+
id: 'copy',
|
|
2028
|
+
label: t('menu.copy'),
|
|
2029
|
+
icon: h(CopyIcon, { size: 15 }),
|
|
2030
|
+
disabled: !hasText,
|
|
2031
|
+
run: async () => {
|
|
2032
|
+
if (!hasText) {
|
|
2033
|
+
notify(t('toast.empty'));
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
await copyText(text);
|
|
2037
|
+
notify(t('toast.copied'));
|
|
2038
|
+
},
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
id: 'quote',
|
|
2042
|
+
label: t('menu.quote'),
|
|
2043
|
+
icon: h(QuoteIcon, { size: 15 }),
|
|
2044
|
+
disabled: !hasText,
|
|
2045
|
+
run: async () => {
|
|
2046
|
+
if (!hasText) {
|
|
2047
|
+
notify(t('toast.empty'));
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
await copyText(quote || text);
|
|
2051
|
+
notify(t('toast.copied'));
|
|
2052
|
+
},
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
id: 'insert',
|
|
2056
|
+
label: t('menu.insert'),
|
|
2057
|
+
icon: h(ComposerIcon, { size: 15 }),
|
|
2058
|
+
disabled: !hasText,
|
|
2059
|
+
run: async () => {
|
|
2060
|
+
if (!hasText) {
|
|
2061
|
+
notify(t('toast.empty'));
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const value = `${quote || text}\n`;
|
|
2065
|
+
if (insertIntoComposer(value)) {
|
|
2066
|
+
notify(t('toast.inserted'));
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
await copyText(value);
|
|
2070
|
+
notify(t('toast.insertFallback'));
|
|
2071
|
+
},
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
id: 'inspect',
|
|
2075
|
+
label: t('menu.inspect'),
|
|
2076
|
+
icon: h(InspectIcon, { size: 15 }),
|
|
2077
|
+
run: async () => {
|
|
2078
|
+
if (row.hasAttribute('data-request-only')) {
|
|
2079
|
+
const requestButton = row.querySelector('button[data-label]');
|
|
2080
|
+
if (requestButton !== null) {
|
|
2081
|
+
requestButton.click();
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
row.click();
|
|
2086
|
+
},
|
|
2087
|
+
},
|
|
2088
|
+
{ type: 'separator' },
|
|
2089
|
+
{
|
|
2090
|
+
id: 'fork',
|
|
2091
|
+
label: t('menu.fork'),
|
|
2092
|
+
icon: h(ForkIcon, { size: 15 }),
|
|
2093
|
+
hint: forkSeq === null ? undefined : t('menu.forkHint'),
|
|
2094
|
+
disabled: forkSeq === null,
|
|
2095
|
+
title: forkSeq === null ? t('menu.noFork') : undefined,
|
|
2096
|
+
run: async () => {
|
|
2097
|
+
await forkAt(ctx, forkSeq, sessionId, notify, t);
|
|
2098
|
+
},
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
id: 'session',
|
|
2102
|
+
label: t('menu.session'),
|
|
2103
|
+
icon: h(HashIcon, { size: 15 }),
|
|
2104
|
+
disabled: !hasSession,
|
|
2105
|
+
run: async () => {
|
|
2106
|
+
await copyText(sessionId);
|
|
2107
|
+
notify(t('toast.sessionCopied'));
|
|
2108
|
+
},
|
|
2109
|
+
},
|
|
2110
|
+
];
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
function buildSessionItems(snapshot, sessionId, t, ctx, notify) {
|
|
2114
|
+
return [
|
|
2115
|
+
{
|
|
2116
|
+
id: 'export',
|
|
2117
|
+
label: t('menu.export'),
|
|
2118
|
+
icon: h(ExportIcon, { size: 15 }),
|
|
2119
|
+
disabled: snapshot === undefined || snapshot === null,
|
|
2120
|
+
run: async () => {
|
|
2121
|
+
try {
|
|
2122
|
+
const payload = {
|
|
2123
|
+
plugin: MODULE_ID,
|
|
2124
|
+
version: VERSION,
|
|
2125
|
+
exportedAt: new Date().toISOString(),
|
|
2126
|
+
sessionId: sessionId ?? null,
|
|
2127
|
+
trajectory: jsonSafe(snapshot, new Set()),
|
|
2128
|
+
};
|
|
2129
|
+
downloadJson(`dsh-powerup-trajectory-${shortId(sessionId)}-${Date.now()}.json`, payload);
|
|
2130
|
+
notify(t('toast.exported'));
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
notify(format(t('toast.exportFailed'), { message: error?.message ?? String(error) }));
|
|
2133
|
+
}
|
|
2134
|
+
},
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
id: 'session',
|
|
2138
|
+
label: t('menu.session'),
|
|
2139
|
+
icon: h(HashIcon, { size: 15 }),
|
|
2140
|
+
disabled: typeof sessionId !== 'string' || sessionId === '',
|
|
2141
|
+
run: async () => {
|
|
2142
|
+
await copyText(sessionId ?? '');
|
|
2143
|
+
notify(t('toast.sessionCopied'));
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
{ type: 'separator' },
|
|
2147
|
+
{
|
|
2148
|
+
id: 'panel',
|
|
2149
|
+
label: t('menu.panel'),
|
|
2150
|
+
icon: h(LayoutIcon, { size: 15 }),
|
|
2151
|
+
run: async () => {
|
|
2152
|
+
const layout = ctx.get('layout');
|
|
2153
|
+
if (layout !== undefined && typeof layout.selectPanel === 'function') layout.selectPanel('powerup');
|
|
2154
|
+
},
|
|
2155
|
+
},
|
|
2156
|
+
];
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
async function forkAt(ctx, seq, sessionId, notify, t) {
|
|
2160
|
+
const sessions = ctx?.get?.('sessions');
|
|
2161
|
+
if (sessions === undefined || typeof sessions.fork !== 'function') {
|
|
2162
|
+
notify(t('toast.forkUnavailable'));
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
2166
|
+
notify(t('toast.noSession'));
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
let childId;
|
|
2170
|
+
try {
|
|
2171
|
+
childId = await sessions.fork({ sessionId, atSeq: seq, increaseTitle: true });
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
notify(format(t('toast.forkFailed'), { message: error?.message ?? String(error) }));
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
const workspace = ctx.get('uiWorkspace');
|
|
2178
|
+
if (workspace !== undefined && typeof workspace.openSession === 'function') workspace.openSession(childId);
|
|
2179
|
+
} catch {
|
|
2180
|
+
/* The fork exists even when the UI cannot open it immediately. */
|
|
2181
|
+
}
|
|
2182
|
+
notify(t('toast.forked'));
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// ------------------------------------------------------------------
|
|
2186
|
+
// Sidebar page
|
|
2187
|
+
// ------------------------------------------------------------------
|
|
2188
|
+
function PowerUpPage({ ctx }) {
|
|
2189
|
+
const settings = useSettings();
|
|
2190
|
+
const [, forceLocale] = useState(0);
|
|
2191
|
+
useEffect(() => {
|
|
2192
|
+
const locale = ctx.get('locale');
|
|
2193
|
+
if (locale === undefined || typeof locale.subscribe !== 'function') return undefined;
|
|
2194
|
+
return locale.subscribe(() => forceLocale((value) => value + 1));
|
|
2195
|
+
}, [ctx]);
|
|
2196
|
+
const t = (key, params) => translateCurrent(key, params);
|
|
2197
|
+
const actions = [
|
|
2198
|
+
{ id: 'copy', icon: CopyIcon, name: t('action.copy'), hint: t('action.copyHint') },
|
|
2199
|
+
{ id: 'quote', icon: QuoteIcon, name: t('action.quote'), hint: t('action.quoteHint') },
|
|
2200
|
+
{ id: 'insert', icon: ComposerIcon, name: t('action.insert'), hint: t('action.insertHint') },
|
|
2201
|
+
{ id: 'inspect', icon: InspectIcon, name: t('action.inspect'), hint: t('action.inspectHint') },
|
|
2202
|
+
{ id: 'fork', icon: ForkIcon, name: t('action.fork'), hint: t('action.forkHint') },
|
|
2203
|
+
{ id: 'export', icon: ExportIcon, name: t('action.export'), hint: t('action.exportHint') },
|
|
2204
|
+
];
|
|
2205
|
+
return h('div', { className: 'dsh-powerup-page' },
|
|
2206
|
+
h('div', { className: 'dsh-powerup-shell' },
|
|
2207
|
+
h('header', { className: 'dsh-powerup-hero' },
|
|
2208
|
+
h('div', { className: 'dsh-powerup-hero-art' }, h(BrandMark, { size: 64 })),
|
|
2209
|
+
h('div', { className: 'dsh-powerup-hero-copy' },
|
|
2210
|
+
h('h1', null, t('page.title')),
|
|
2211
|
+
h('p', null, t('page.subtitle')),
|
|
2212
|
+
),
|
|
2213
|
+
),
|
|
2214
|
+
h('section', { className: 'dsh-powerup-card' },
|
|
2215
|
+
h('div', { className: 'dsh-powerup-card-head' },
|
|
2216
|
+
h('div', { className: 'dsh-powerup-card-icon' }, h(PowerUpLineIcon, { size: 18 })),
|
|
2217
|
+
h('div', { className: 'dsh-powerup-card-copy' },
|
|
2218
|
+
h('h2', null, t('page.featureTitle')),
|
|
2219
|
+
h('p', null, t('page.featureDesc')),
|
|
2220
|
+
),
|
|
2221
|
+
h('button', {
|
|
2222
|
+
type: 'button',
|
|
2223
|
+
role: 'switch',
|
|
2224
|
+
'aria-checked': settings.enabled ? 'true' : 'false',
|
|
2225
|
+
'aria-label': t('page.featureTitle'),
|
|
2226
|
+
className: 'dsh-powerup-switch',
|
|
2227
|
+
onClick: () => setSettings({ enabled: !settings.enabled }),
|
|
2228
|
+
},
|
|
2229
|
+
h('span', { className: 'dsh-powerup-switch-track' },
|
|
2230
|
+
h('span', { className: 'dsh-powerup-switch-thumb' }),
|
|
2231
|
+
),
|
|
2232
|
+
h('span', null, settings.enabled ? t('page.enabled') : t('page.disabled')),
|
|
2233
|
+
),
|
|
2234
|
+
),
|
|
2235
|
+
h('div', { style: { marginTop: '14px' } },
|
|
2236
|
+
h('h3', { className: 'dsh-powerup-section-title' }, t('page.actions')),
|
|
2237
|
+
h('ul', { className: 'dsh-powerup-actions' },
|
|
2238
|
+
actions.map((action) => h('li', { key: action.id, className: 'dsh-powerup-action' },
|
|
2239
|
+
h('span', { className: 'dsh-powerup-action-icon' }, h(action.icon, { size: 15 })),
|
|
2240
|
+
h('span', { className: 'dsh-powerup-action-copy' },
|
|
2241
|
+
h('span', { className: 'dsh-powerup-action-name' }, action.name),
|
|
2242
|
+
h('span', { className: 'dsh-powerup-action-hint' }, action.hint),
|
|
2243
|
+
),
|
|
2244
|
+
)),
|
|
2245
|
+
),
|
|
2246
|
+
),
|
|
2247
|
+
),
|
|
2248
|
+
h('section', { className: 'dsh-powerup-card' },
|
|
2249
|
+
h('div', { className: 'dsh-powerup-card-head' },
|
|
2250
|
+
h('div', { className: 'dsh-powerup-card-icon' }, h(UpdateIcon, { size: 18 })),
|
|
2251
|
+
h('div', { className: 'dsh-powerup-card-copy' },
|
|
2252
|
+
h('h2', null, t('page.updateTitle')),
|
|
2253
|
+
h('p', null, t('page.updateDesc')),
|
|
2254
|
+
),
|
|
2255
|
+
),
|
|
2256
|
+
h('p', { className: 'dsh-powerup-update-note', style: { marginTop: '12px' } }, t('page.updateHow')),
|
|
2257
|
+
),
|
|
2258
|
+
h('section', { className: 'dsh-powerup-card' },
|
|
2259
|
+
h('h2', { className: 'dsh-powerup-section-title', style: { marginBottom: '10px' } }, t('page.howTitle')),
|
|
2260
|
+
h('ol', { className: 'dsh-powerup-steps' },
|
|
2261
|
+
h('li', { className: 'dsh-powerup-step' },
|
|
2262
|
+
h('span', { className: 'dsh-powerup-step-num' }, '1'),
|
|
2263
|
+
h('span', null, t('page.how1')),
|
|
2264
|
+
),
|
|
2265
|
+
h('li', { className: 'dsh-powerup-step' },
|
|
2266
|
+
h('span', { className: 'dsh-powerup-step-num' }, '2'),
|
|
2267
|
+
h('span', null, t('page.how2')),
|
|
2268
|
+
),
|
|
2269
|
+
h('li', { className: 'dsh-powerup-step' },
|
|
2270
|
+
h('span', { className: 'dsh-powerup-step-num' }, '3'),
|
|
2271
|
+
h('span', null, t('page.how3')),
|
|
2272
|
+
),
|
|
2273
|
+
),
|
|
2274
|
+
),
|
|
2275
|
+
h('footer', { className: 'dsh-powerup-foot' },
|
|
2276
|
+
h('span', null, format(t('page.footer'), { version: VERSION })),
|
|
2277
|
+
h('span', { className: 'dsh-powerup-dot' }),
|
|
2278
|
+
h('span', null, t('page.localOnly')),
|
|
2279
|
+
),
|
|
2280
|
+
),
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// ------------------------------------------------------------------
|
|
2285
|
+
// Translator wiring
|
|
2286
|
+
// ------------------------------------------------------------------
|
|
2287
|
+
let translateCurrent = makeFallbackTranslate();
|
|
2288
|
+
|
|
2289
|
+
function bindLocale(ctx) {
|
|
2290
|
+
const locale = ctx.get('locale');
|
|
2291
|
+
if (locale === undefined || typeof locale.register !== 'function') return;
|
|
2292
|
+
try {
|
|
2293
|
+
const dispose = locale.register(NS, DICT);
|
|
2294
|
+
if (typeof locale.bind === 'function') translateCurrent = locale.bind(NS);
|
|
2295
|
+
ctx.effect(() => () => {
|
|
2296
|
+
translateCurrent = makeFallbackTranslate();
|
|
2297
|
+
try {
|
|
2298
|
+
dispose?.();
|
|
2299
|
+
} catch {
|
|
2300
|
+
/* Removing a dictionary must never break unload. */
|
|
2301
|
+
}
|
|
2302
|
+
}, 'dsh-powerup: dictionaries');
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
console.warn('[dsh-powerup] locale registration failed:', error?.message ?? error);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// ------------------------------------------------------------------
|
|
2309
|
+
// Plugin body
|
|
2310
|
+
// ------------------------------------------------------------------
|
|
2311
|
+
function apply(ctx) {
|
|
2312
|
+
bindLocale(ctx);
|
|
2313
|
+
installStyles(ctx);
|
|
2314
|
+
|
|
2315
|
+
const slots = ctx.get('slots');
|
|
2316
|
+
if (slots === undefined || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
2317
|
+
console.warn('[dsh-powerup] slots service missing; Power-Up UI disabled');
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const register = (slot, options, component, label) => {
|
|
2322
|
+
try {
|
|
2323
|
+
ctx.effect(
|
|
2324
|
+
() => slots.inject(slot, () => slots.register({ name: slot, ...options }, component)),
|
|
2325
|
+
label,
|
|
2326
|
+
);
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
console.warn(`[dsh-powerup] could not register ${slot}:`, error?.message ?? error);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
register('sidebar.panellist', {
|
|
2333
|
+
id: 'powerup',
|
|
2334
|
+
order: 30,
|
|
2335
|
+
label: () => translateCurrent('panel.title'),
|
|
2336
|
+
}, (props) => h(PowerUpLineIcon, { size: props?.size ?? 16 }), 'dsh-powerup: sidebar entry');
|
|
2337
|
+
|
|
2338
|
+
register('main', { key: 'powerup' }, () => h(PowerUpPage, { ctx }), 'dsh-powerup: main page');
|
|
2339
|
+
|
|
2340
|
+
register('shell.overlay', {
|
|
2341
|
+
id: 'powerup-context-menu',
|
|
2342
|
+
order: 55,
|
|
2343
|
+
}, () => h(PowerUpOverlay, { ctx }), 'dsh-powerup: context menu');
|
|
2344
|
+
|
|
2345
|
+
register('conversation.session.header.utilities', {
|
|
2346
|
+
id: 'powerup-bridge',
|
|
2347
|
+
order: 50,
|
|
2348
|
+
}, PowerUpBridge, 'dsh-powerup: session bridge');
|
|
2349
|
+
|
|
2350
|
+
// The update control drives the built-in plugin manager through its
|
|
2351
|
+
// `remote.pluginManager` namespace. Register it in a nested inject scope
|
|
2352
|
+
// so a late-provided service is observed instead of sampled once.
|
|
2353
|
+
if (typeof ctx.inject === 'function') {
|
|
2354
|
+
try {
|
|
2355
|
+
ctx.inject(['remote.pluginManager'], (scope) => {
|
|
2356
|
+
const manager = scope.get('remote.pluginManager');
|
|
2357
|
+
const remote = scope.get('remote');
|
|
2358
|
+
const childSlots = scope.get('slots');
|
|
2359
|
+
if (manager === undefined || childSlots === undefined || typeof childSlots.register !== 'function') return;
|
|
2360
|
+
scope.effect(
|
|
2361
|
+
() => childSlots.inject('shell.overlay', () => childSlots.register({
|
|
2362
|
+
name: 'shell.overlay',
|
|
2363
|
+
id: 'powerup-plugin-update',
|
|
2364
|
+
order: 56,
|
|
2365
|
+
}, () => h(PluginUpdateOverlay, { manager, remote, ctx: scope }))),
|
|
2366
|
+
'dsh-powerup: plugin update',
|
|
2367
|
+
);
|
|
2368
|
+
});
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
console.warn('[dsh-powerup] plugin update registration failed:', error?.message ?? error);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
return {
|
|
2376
|
+
inject: ['slots', 'locale'],
|
|
2377
|
+
apply,
|
|
2378
|
+
// Exported for smoke tests only; the host never reads this.
|
|
2379
|
+
__test: {
|
|
2380
|
+
DICT,
|
|
2381
|
+
CSS,
|
|
2382
|
+
PowerUpLineIcon,
|
|
2383
|
+
UpdateIcon,
|
|
2384
|
+
PowerUpBridge,
|
|
2385
|
+
PowerUpPage,
|
|
2386
|
+
PowerUpOverlay,
|
|
2387
|
+
PluginUpdateOverlay,
|
|
2388
|
+
ensureUpdateButtons,
|
|
2389
|
+
updateButtonLabel,
|
|
2390
|
+
rowKeyParts,
|
|
2391
|
+
resolveForkSeq,
|
|
2392
|
+
markdownQuote,
|
|
2393
|
+
jsonSafe,
|
|
2394
|
+
buildRowItems,
|
|
2395
|
+
buildSessionItems,
|
|
2396
|
+
sortVersions,
|
|
2397
|
+
compareVersionsDesc,
|
|
2398
|
+
buildRegistryChoices,
|
|
2399
|
+
pickRegistryChoice,
|
|
2400
|
+
lookupRegistries,
|
|
2401
|
+
fetchRegistryVersions,
|
|
2402
|
+
registryHost,
|
|
2403
|
+
normalizeRegistry,
|
|
2404
|
+
readRememberedRegistry,
|
|
2405
|
+
packageDisplayTitle,
|
|
2406
|
+
normalizeVersionInput,
|
|
2407
|
+
inspectReason,
|
|
2408
|
+
operationReason,
|
|
2409
|
+
},
|
|
2410
|
+
};
|
|
2411
|
+
},
|
|
2412
|
+
});
|
|
2413
|
+
})();
|