dsh-human-wait 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +26 -8
- package/client.js +363 -0
- package/cordis.patch.yml +6 -0
- package/index.js +49 -16
- package/package.json +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 — 2026-10-27
|
|
4
|
+
|
|
5
|
+
- Adds a browser half (`client.js`), so the plugin gets its own settings page under **Plugins**:
|
|
6
|
+
a wait-cap field with 1 h / 24 h / unlimited quick values, an "overridden" chip and a
|
|
7
|
+
"restore default" action.
|
|
8
|
+
- `enabled` and `humanWaitCapMs` are now volatile: they are re-read while the plugin runs, so a
|
|
9
|
+
save applies in about 1.5 s without restarting DSH. The wrappers also check `enabled` on
|
|
10
|
+
every call, so turning the plugin off through the patch takes effect immediately.
|
|
11
|
+
- The host half takes over the settings page (`settings.configure({ auto: false })`) instead of
|
|
12
|
+
letting DSH auto-generate a second, schema-only one.
|
|
13
|
+
- Tests: adds `test/client.test.mjs` (bundle id, slot key, page and summary rendering) and two
|
|
14
|
+
runtime cases for re-reading the settings.
|
|
15
|
+
|
|
16
|
+
## 0.1.1 — 2026-10-26
|
|
17
|
+
|
|
18
|
+
First release published by GitHub Actions with npm provenance (trusted publishing).
|
|
19
|
+
|
|
20
|
+
- No behaviour change from 0.1.0; the source is the same apart from this entry and the docs.
|
|
21
|
+
- README: installing from the registry is now the primary route, the tarball route moved to the
|
|
22
|
+
fallback slot.
|
|
23
|
+
|
|
3
24
|
## 0.1.0 — 2026-10-26
|
|
4
25
|
|
|
5
26
|
First release.
|
package/README.md
CHANGED
|
@@ -42,15 +42,20 @@ execution budget and is what the plugin enforces.
|
|
|
42
42
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
45
|
-
The plugin is a normal DSH bundle
|
|
45
|
+
The plugin is a normal DSH bundle: one inserted host row, plus a browser half that renders the
|
|
46
|
+
settings page under **Plugins**. Pick one route:
|
|
46
47
|
|
|
47
|
-
**A. From
|
|
48
|
+
**A. From the registry (recommended).** Use the Plugins settings page, or a
|
|
49
|
+
`plugin_manager install_bundle` call in a session, with the spec `dsh-human-wait@0.2.1`; the
|
|
50
|
+
plugin manager adds both the dependency and the bundle entry for you.
|
|
51
|
+
|
|
52
|
+
**B. From a tarball, by hand.** In `<DSH_HOME>/profiles/<profile>/package.json` add the dependency
|
|
48
53
|
and the bundle name, then install it:
|
|
49
54
|
|
|
50
55
|
```jsonc
|
|
51
56
|
{
|
|
52
57
|
"dependencies": {
|
|
53
|
-
"dsh-human-wait": "file:D:/path/to/dsh-human-wait-0.1.
|
|
58
|
+
"dsh-human-wait": "file:D:/path/to/dsh-human-wait-0.2.1.tgz"
|
|
54
59
|
},
|
|
55
60
|
"dsh": { "profile": { "bundles": ["dsh-human-wait"] } }
|
|
56
61
|
}
|
|
@@ -61,10 +66,6 @@ and the bundle name, then install it:
|
|
|
61
66
|
& "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\pnpm.cjs" install --dir "$env:USERPROFILE\.dsh\profiles\desktop"
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
**B. From the registry, once published.** Use the Plugins settings page (or a
|
|
65
|
-
`plugin_manager install_bundle` call in a session) with the spec `dsh-human-wait@0.1.0`; the
|
|
66
|
-
plugin manager adds both the dependency and the bundle entry for you.
|
|
67
|
-
|
|
68
69
|
Then **quit DSH from the tray icon and start it again** — bundles are composed at boot. The
|
|
69
70
|
Electron-managed profile cannot be booted through the `dsh` CLI (`profile "desktop" is managed
|
|
70
71
|
exclusively by the Electron application`), so restarting the app is the only way to apply it.
|
|
@@ -93,7 +94,24 @@ How the clock works: the plugin re-arms a single timer on every accounting event
|
|
|
93
94
|
wait starting or ending, or the timer itself firing), so there is no fixed sampling period and
|
|
94
95
|
no configuration knob for one.
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
Both options are **volatile**: they are read again while the plugin is running, so a change
|
|
98
|
+
takes effect within about a second and a half, with no restart. Editing the
|
|
99
|
+
patch by hand is still read at boot.
|
|
100
|
+
|
|
101
|
+
## Settings page
|
|
102
|
+
|
|
103
|
+
The plugin ships a browser half, so it gets its own card on its page under **Plugins** (the same
|
|
104
|
+
place as Team limits):
|
|
105
|
+
|
|
106
|
+
- a number field for `humanWaitCapMs`, with quick buttons for 1 hour / 24 hours / unlimited;
|
|
107
|
+
- an "overridden" chip and a "restore default" action for that field;
|
|
108
|
+
- a one-line summary on the card itself.
|
|
109
|
+
|
|
110
|
+
`enabled` stays in the patch layer (`cordis.patch.yml`), where it is read live within about a
|
|
111
|
+
second and a half.
|
|
112
|
+
|
|
113
|
+
The values are written back to this profile's patch layer through the normal settings path;
|
|
114
|
+
uninstalling the plugin does not roll them back.
|
|
97
115
|
|
|
98
116
|
## What counts as a human wait
|
|
99
117
|
|
package/client.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-human-wait',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
|
|
7
|
+
var React = require('react')
|
|
8
|
+
var h = React.createElement
|
|
9
|
+
|
|
10
|
+
var NS = 'human-wait'
|
|
11
|
+
|
|
12
|
+
var DEFAULTS = {
|
|
13
|
+
humanWaitCapMs: 86400000,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var HOUR = 3600000
|
|
17
|
+
|
|
18
|
+
var QUICK_CAPS = [
|
|
19
|
+
{ value: HOUR, zh: '1 小时', en: '1 hour' },
|
|
20
|
+
{ value: 24 * HOUR, zh: '24 小时', en: '24 hours' },
|
|
21
|
+
{ value: -1, zh: '无限', en: 'Unlimited' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function isZh() {
|
|
25
|
+
try {
|
|
26
|
+
return (navigator.language || '').toLowerCase().indexOf('zh') === 0
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var COPY = {
|
|
33
|
+
capLabel: ['等待上限(毫秒)', 'Wait cap (ms)'],
|
|
34
|
+
capHelp: [
|
|
35
|
+
'一次运行最多在等待人上花多久。-1 表示不设上限。',
|
|
36
|
+
'How long one run may spend waiting for a human. -1 means no cap.',
|
|
37
|
+
],
|
|
38
|
+
overridden: ['已覆盖', 'Overridden'],
|
|
39
|
+
reset: ['恢复默认', 'Restore default'],
|
|
40
|
+
loading: ['正在读取配置…', 'Loading configuration…'],
|
|
41
|
+
unavailable: [
|
|
42
|
+
'当前连接不支持写入设置,这一页只能查看。',
|
|
43
|
+
'This connection cannot write settings, so this page is read-only.',
|
|
44
|
+
],
|
|
45
|
+
saveFailed: ['保存失败:', 'Save failed: '],
|
|
46
|
+
note: [
|
|
47
|
+
'数值保存在当前 profile 的补丁层;卸载本插件不会自动撤回。',
|
|
48
|
+
'Values persist in this profile patch layer; uninstalling the plugin does not roll them back.',
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function t(key) {
|
|
53
|
+
var pair = COPY[key]
|
|
54
|
+
if (pair === undefined) return key
|
|
55
|
+
return isZh() ? pair[0] : pair[1]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var CSS = [
|
|
59
|
+
'.dhw { display: flex; flex-direction: column; gap: 26px; max-width: 820px; padding: 4px 2px 8px; }',
|
|
60
|
+
'.dhw-group { display: flex; flex-direction: column; gap: 14px; }',
|
|
61
|
+
'.dhw-group-title { font-size: 14.5px; font-weight: 600; margin: 0; line-height: 1.4; }',
|
|
62
|
+
'.dhw-cell { display: flex; flex-direction: column; gap: 8px; min-width: 0; }',
|
|
63
|
+
'.dhw-labelrow { display: flex; align-items: center; gap: 8px; min-height: 20px; }',
|
|
64
|
+
'.dhw-label { font-size: 13px; font-weight: 500; }',
|
|
65
|
+
'.dhw-chip { font-size: 11.5px; line-height: 18px; padding: 0 8px; border-radius: 999px; background: rgba(128,128,128,.14); opacity: .8; }',
|
|
66
|
+
'.dhw-spacer { flex: 1 1 auto; }',
|
|
67
|
+
'.dhw-reset { font-size: 12.5px; color: inherit; opacity: .55; background: none; border: none; padding: 0; cursor: pointer; }',
|
|
68
|
+
'.dhw-reset:hover:not(:disabled) { opacity: .85; text-decoration: underline; }',
|
|
69
|
+
'.dhw-reset:disabled { opacity: .25; cursor: default; }',
|
|
70
|
+
'.dhw-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; font-size: 13.5px; font-variant-numeric: tabular-nums; border-radius: 9px; border: 1px solid rgba(128,128,128,.36); background: transparent; color: inherit; outline: none; }',
|
|
71
|
+
'.dhw-input:focus { border-color: rgba(76,139,245,.85); }',
|
|
72
|
+
'.dhw-input:disabled { opacity: .45; }',
|
|
73
|
+
'.dhw-quick { display: flex; flex-wrap: wrap; gap: 8px; }',
|
|
74
|
+
'.dhw-quickbtn { font-size: 12.5px; padding: 5px 11px; border-radius: 999px; border: 1px solid rgba(128,128,128,.36); background: transparent; color: inherit; cursor: pointer; }',
|
|
75
|
+
'.dhw-quickbtn:hover:not(:disabled) { border-color: rgba(76,139,245,.7); }',
|
|
76
|
+
'.dhw-quickbtn:disabled { opacity: .4; cursor: default; }',
|
|
77
|
+
'.dhw-help { font-size: 12px; opacity: .5; line-height: 1.5; }',
|
|
78
|
+
'.dhw-foot { font-size: 12px; opacity: .48; line-height: 1.6; margin: 0; max-width: 64ch; }',
|
|
79
|
+
'.dhw-error { font-size: 12.5px; color: #d9534f; }',
|
|
80
|
+
'.dhw-summary { font-size: 13px; opacity: .78; font-variant-numeric: tabular-nums; }',
|
|
81
|
+
].join('\n')
|
|
82
|
+
|
|
83
|
+
/** Unwrap a volatile ref if the host handed one through. */
|
|
84
|
+
function unwrap(value) {
|
|
85
|
+
if (value !== null && typeof value === 'object') {
|
|
86
|
+
if (typeof value.get === 'function') return value.get()
|
|
87
|
+
if (typeof value.value !== 'undefined') return value.value
|
|
88
|
+
}
|
|
89
|
+
return value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function capOf(value, fallback) {
|
|
93
|
+
var raw = unwrap(value)
|
|
94
|
+
if (raw === -1) return -1
|
|
95
|
+
var parsed = typeof raw === 'number' ? raw : Number(raw)
|
|
96
|
+
return Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : fallback
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function valuesOf(source) {
|
|
100
|
+
var bag = source !== null && typeof source === 'object' ? source : {}
|
|
101
|
+
return {
|
|
102
|
+
humanWaitCapMs: capOf(bag.humanWaitCapMs, DEFAULTS.humanWaitCapMs),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatCap(ms) {
|
|
107
|
+
if (ms === -1) return isZh() ? '无限' : 'unlimited'
|
|
108
|
+
if (ms % HOUR === 0) {
|
|
109
|
+
var hours = ms / HOUR
|
|
110
|
+
return isZh() ? hours + ' 小时' : hours + (hours === 1 ? ' hour' : ' hours')
|
|
111
|
+
}
|
|
112
|
+
return ms + ' ms'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function HumanWaitPage(props) {
|
|
116
|
+
var form = props.configForm || props.form
|
|
117
|
+
var summary = props.view === 'summary'
|
|
118
|
+
|
|
119
|
+
if (!form) {
|
|
120
|
+
return h('p', { className: 'dhw-foot' }, t('loading'))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var snapshotPair = React.useState(function () {
|
|
124
|
+
return form.getSnapshot()
|
|
125
|
+
})
|
|
126
|
+
var snapshot = snapshotPair[0]
|
|
127
|
+
var setSnapshot = snapshotPair[1]
|
|
128
|
+
|
|
129
|
+
var draftPair = React.useState(function () {
|
|
130
|
+
return valuesOf(snapshot.value)
|
|
131
|
+
})
|
|
132
|
+
var draft = draftPair[0]
|
|
133
|
+
var setDraft = draftPair[1]
|
|
134
|
+
|
|
135
|
+
var errorPair = React.useState('')
|
|
136
|
+
var error = errorPair[0]
|
|
137
|
+
var setError = errorPair[1]
|
|
138
|
+
|
|
139
|
+
var pendingPair = React.useState({})
|
|
140
|
+
var pending = pendingPair[0]
|
|
141
|
+
var setPending = pendingPair[1]
|
|
142
|
+
|
|
143
|
+
React.useEffect(function () {
|
|
144
|
+
var off = form.subscribe(function () {
|
|
145
|
+
setSnapshot(form.getSnapshot())
|
|
146
|
+
})
|
|
147
|
+
setSnapshot(form.getSnapshot())
|
|
148
|
+
return off
|
|
149
|
+
}, [form])
|
|
150
|
+
|
|
151
|
+
React.useEffect(function () {
|
|
152
|
+
var next = valuesOf(snapshot.value)
|
|
153
|
+
var keys = Object.keys(pending)
|
|
154
|
+
setDraft(function (previous) {
|
|
155
|
+
var merged = {}
|
|
156
|
+
for (var name in previous) merged[name] = previous[name]
|
|
157
|
+
for (var name in next) {
|
|
158
|
+
if (keys.indexOf(name) < 0) merged[name] = next[name]
|
|
159
|
+
}
|
|
160
|
+
return merged
|
|
161
|
+
})
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
}, [snapshot.value])
|
|
164
|
+
|
|
165
|
+
React.useEffect(function () {
|
|
166
|
+
var keys = Object.keys(pending)
|
|
167
|
+
if (keys.length === 0) return undefined
|
|
168
|
+
var timer = setTimeout(function () {
|
|
169
|
+
var chain = Promise.resolve()
|
|
170
|
+
keys.forEach(function (key) {
|
|
171
|
+
chain = chain.then(function () {
|
|
172
|
+
return form.set(key, pending[key])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
chain
|
|
176
|
+
.then(function () {
|
|
177
|
+
setPending({})
|
|
178
|
+
})
|
|
179
|
+
.catch(function (caught) {
|
|
180
|
+
setPending({})
|
|
181
|
+
setError(t('saveFailed') + String((caught && caught.message) || caught))
|
|
182
|
+
})
|
|
183
|
+
}, 700)
|
|
184
|
+
return function () {
|
|
185
|
+
clearTimeout(timer)
|
|
186
|
+
}
|
|
187
|
+
}, [pending, form])
|
|
188
|
+
|
|
189
|
+
var ready = snapshot.status === 'ready'
|
|
190
|
+
var disabled = !ready
|
|
191
|
+
|
|
192
|
+
function write(key, value) {
|
|
193
|
+
setError('')
|
|
194
|
+
setPending(function (previous) {
|
|
195
|
+
var next = {}
|
|
196
|
+
for (var name in previous) next[name] = previous[name]
|
|
197
|
+
next[key] = value
|
|
198
|
+
return next
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function onCapInput(raw) {
|
|
203
|
+
if (raw === '' || raw === '-') {
|
|
204
|
+
setDraft(function (previous) {
|
|
205
|
+
var merged = {}
|
|
206
|
+
for (var name in previous) merged[name] = previous[name]
|
|
207
|
+
merged.humanWaitCapMs = raw === '-' ? -1 : DEFAULTS.humanWaitCapMs
|
|
208
|
+
return merged
|
|
209
|
+
})
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
var parsed = Number(raw)
|
|
213
|
+
if (!Number.isFinite(parsed)) return
|
|
214
|
+
var safe = parsed === -1 ? -1 : Math.max(1, Math.floor(parsed))
|
|
215
|
+
setDraft(function (previous) {
|
|
216
|
+
var merged = {}
|
|
217
|
+
for (var name in previous) merged[name] = previous[name]
|
|
218
|
+
merged.humanWaitCapMs = safe
|
|
219
|
+
return merged
|
|
220
|
+
})
|
|
221
|
+
write('humanWaitCapMs', safe)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function onQuick(value) {
|
|
225
|
+
setDraft(function (previous) {
|
|
226
|
+
var merged = {}
|
|
227
|
+
for (var name in previous) merged[name] = previous[name]
|
|
228
|
+
merged.humanWaitCapMs = value
|
|
229
|
+
return merged
|
|
230
|
+
})
|
|
231
|
+
write('humanWaitCapMs', value)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onReset(key) {
|
|
235
|
+
setDraft(function (previous) {
|
|
236
|
+
var merged = {}
|
|
237
|
+
for (var name in previous) merged[name] = previous[name]
|
|
238
|
+
merged[key] = DEFAULTS[key]
|
|
239
|
+
return merged
|
|
240
|
+
})
|
|
241
|
+
write(key, DEFAULTS[key])
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (summary) {
|
|
245
|
+
return h('span', { className: 'dhw-summary' }, formatCap(draft.humanWaitCapMs))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
var capOverridden = draft.humanWaitCapMs !== DEFAULTS.humanWaitCapMs
|
|
249
|
+
|
|
250
|
+
var capGroup = h(
|
|
251
|
+
'section',
|
|
252
|
+
{ className: 'dhw-group', key: 'cap' },
|
|
253
|
+
h('h3', { className: 'dhw-group-title' }, isZh() ? '等待上限' : 'Wait cap'),
|
|
254
|
+
h(
|
|
255
|
+
'div',
|
|
256
|
+
{ className: 'dhw-cell' },
|
|
257
|
+
h(
|
|
258
|
+
'div',
|
|
259
|
+
{ className: 'dhw-labelrow' },
|
|
260
|
+
h('label', { className: 'dhw-label', htmlFor: 'dhw-cap' }, t('capLabel')),
|
|
261
|
+
capOverridden ? h('span', { className: 'dhw-chip' }, t('overridden')) : null,
|
|
262
|
+
h('span', { className: 'dhw-spacer' }),
|
|
263
|
+
capOverridden
|
|
264
|
+
? h(
|
|
265
|
+
'button',
|
|
266
|
+
{
|
|
267
|
+
type: 'button',
|
|
268
|
+
className: 'dhw-reset',
|
|
269
|
+
disabled: disabled,
|
|
270
|
+
onClick: function () {
|
|
271
|
+
onReset('humanWaitCapMs')
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
t('reset'),
|
|
275
|
+
)
|
|
276
|
+
: null,
|
|
277
|
+
),
|
|
278
|
+
h('input', {
|
|
279
|
+
id: 'dhw-cap',
|
|
280
|
+
className: 'dhw-input',
|
|
281
|
+
type: 'number',
|
|
282
|
+
min: -1,
|
|
283
|
+
step: 1,
|
|
284
|
+
inputMode: 'numeric',
|
|
285
|
+
disabled: disabled,
|
|
286
|
+
value: String(draft.humanWaitCapMs),
|
|
287
|
+
onChange: function (event) {
|
|
288
|
+
onCapInput(event.target.value)
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
h(
|
|
292
|
+
'div',
|
|
293
|
+
{ className: 'dhw-quick' },
|
|
294
|
+
QUICK_CAPS.map(function (item) {
|
|
295
|
+
return h(
|
|
296
|
+
'button',
|
|
297
|
+
{
|
|
298
|
+
type: 'button',
|
|
299
|
+
className: 'dhw-quickbtn',
|
|
300
|
+
key: item.value,
|
|
301
|
+
disabled: disabled,
|
|
302
|
+
onClick: function () {
|
|
303
|
+
onQuick(item.value)
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
isZh() ? item.zh : item.en,
|
|
307
|
+
)
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
h('span', { className: 'dhw-help' }, t('capHelp')),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return h(
|
|
315
|
+
'div',
|
|
316
|
+
{ className: 'dhw' },
|
|
317
|
+
h('style', null, CSS),
|
|
318
|
+
ready ? null : h('p', { className: 'dhw-foot' }, t(snapshot.status === 'unavailable' ? 'unavailable' : 'loading')),
|
|
319
|
+
capGroup,
|
|
320
|
+
error ? h('p', { className: 'dhw-error' }, error) : null,
|
|
321
|
+
h('p', { className: 'dhw-foot' }, t('note')),
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function apply(ctx) {
|
|
326
|
+
try {
|
|
327
|
+
var forms = ctx.configForms
|
|
328
|
+
var form = forms ? forms.get(NS) : null
|
|
329
|
+
if (!form) {
|
|
330
|
+
console.error('[human-wait] configForms service is unavailable; no settings page is registered')
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
ctx.effect(
|
|
334
|
+
function () {
|
|
335
|
+
return forms.whileServed([NS], function () {
|
|
336
|
+
return ctx.slots.inject('plugins.bundle.config', function () {
|
|
337
|
+
return ctx.slots.register(
|
|
338
|
+
{
|
|
339
|
+
name: 'plugins.bundle.config',
|
|
340
|
+
key: 'dsh-human-wait',
|
|
341
|
+
inject: function () {
|
|
342
|
+
return { configForm: form }
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
HumanWaitPage,
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
},
|
|
350
|
+
'human-wait: bundle config',
|
|
351
|
+
)
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.error('[human-wait] client half failed to mount: ' + String((error && error.message) || error))
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
exports.name = 'dsh-human-wait'
|
|
358
|
+
exports.inject = ['slots', 'configForms']
|
|
359
|
+
exports.apply = apply
|
|
360
|
+
|
|
361
|
+
return module.exports
|
|
362
|
+
},
|
|
363
|
+
})
|
package/cordis.patch.yml
CHANGED
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
# `timeoutMs` passed to a single `run_code` call) is still honoured for the
|
|
18
18
|
# time the program actually spends executing: the plugin re-arms the deadline
|
|
19
19
|
# on every accounting event, so it does not resample on a timer.
|
|
20
|
+
#
|
|
21
|
+
# Both options are volatile and re-read while the plugin runs, so the wait cap
|
|
22
|
+
# saved from the plugin's page under Settings → Plugins takes effect within
|
|
23
|
+
# about 1.5 s; `enabled` is read from here (patch edits also apply live). The
|
|
24
|
+
# browser half (`client.js`, declared by `dsh.client`) renders that page; this
|
|
25
|
+
# file stays free of user values.
|
|
20
26
|
- insert:
|
|
21
27
|
- id: human-wait
|
|
22
28
|
name: 'dsh-human-wait'
|
package/index.js
CHANGED
|
@@ -35,6 +35,9 @@ const DEFAULT_HUMAN_WAIT_CAP_MS = 24 * 60 * 60 * 1000
|
|
|
35
35
|
const MAX_TIMER_MS = 2_147_483_647
|
|
36
36
|
const DEADLINE_MESSAGE = 'execution deadline reached'
|
|
37
37
|
|
|
38
|
+
/** How often the live settings are compared against the volatile Config refs. */
|
|
39
|
+
const POLL_INTERVAL_MS = 1500
|
|
40
|
+
|
|
38
41
|
/** Per-run wait accounting; a run has a slot only while it is on the stack. */
|
|
39
42
|
const runScope = new AsyncLocalStorage()
|
|
40
43
|
|
|
@@ -51,9 +54,9 @@ try {
|
|
|
51
54
|
export const Config = Schema === undefined
|
|
52
55
|
? undefined
|
|
53
56
|
: Schema.object({
|
|
54
|
-
enabled: Schema.boolean().default(true)
|
|
57
|
+
enabled: Schema.boolean().default(true).volatile()
|
|
55
58
|
.description('Pause the PTC run deadline while a human-interaction wait is pending.'),
|
|
56
|
-
humanWaitCapMs: Schema.number().default(DEFAULT_HUMAN_WAIT_CAP_MS)
|
|
59
|
+
humanWaitCapMs: Schema.number().default(DEFAULT_HUMAN_WAIT_CAP_MS).volatile()
|
|
57
60
|
.description('Longest time a run may spend waiting for a human, in milliseconds. -1 removes the cap.'),
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -195,9 +198,10 @@ function enterHumanWait() {
|
|
|
195
198
|
* Wrap one asynchronous method so callers' time is not charged to the run.
|
|
196
199
|
* @param {object} host
|
|
197
200
|
* @param {string} key
|
|
201
|
+
* @param {{ enabled: boolean }} live
|
|
198
202
|
* @param {object} logger
|
|
199
203
|
*/
|
|
200
|
-
function patchWaitMethod(host, key, logger) {
|
|
204
|
+
function patchWaitMethod(host, key, live, logger) {
|
|
201
205
|
const holder = methodHolder(host, key)
|
|
202
206
|
if (holder === undefined) return undefined
|
|
203
207
|
|
|
@@ -205,6 +209,7 @@ function patchWaitMethod(host, key, logger) {
|
|
|
205
209
|
if (isPatched(original)) return undefined
|
|
206
210
|
|
|
207
211
|
const wrapped = function patchedHumanWait(...args) {
|
|
212
|
+
if (!live.enabled) return Reflect.apply(original, this, args)
|
|
208
213
|
const leave = enterHumanWait()
|
|
209
214
|
let result
|
|
210
215
|
try {
|
|
@@ -246,10 +251,10 @@ function patchWaitMethod(host, key, logger) {
|
|
|
246
251
|
* Wrap the run method: the runtime keeps its compile/validation, the deadline
|
|
247
252
|
* moves into this plugin so it can be paused.
|
|
248
253
|
* @param {object} runtime
|
|
249
|
-
* @param {{ humanWaitCapMs: number }}
|
|
254
|
+
* @param {{ enabled: boolean, humanWaitCapMs: number }} live
|
|
250
255
|
* @param {object} logger
|
|
251
256
|
*/
|
|
252
|
-
function patchRun(runtime,
|
|
257
|
+
function patchRun(runtime, live, logger) {
|
|
253
258
|
const holder = methodHolder(runtime, 'run')
|
|
254
259
|
if (holder === undefined) return undefined
|
|
255
260
|
|
|
@@ -261,12 +266,13 @@ function patchRun(runtime, settings, logger) {
|
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
const wrapped = function patchedRun(spec) {
|
|
269
|
+
if (!live.enabled) return Reflect.apply(original, this, [spec])
|
|
264
270
|
const budget = spec !== null && typeof spec === 'object' ? spec.timeoutMs : undefined
|
|
265
271
|
if (typeof budget !== 'number' || !Number.isFinite(budget) || budget <= 0) {
|
|
266
272
|
return Reflect.apply(original, this, [spec])
|
|
267
273
|
}
|
|
268
274
|
try {
|
|
269
|
-
return runWithPausedDeadline(original, this, spec, budget,
|
|
275
|
+
return runWithPausedDeadline(original, this, spec, budget, live, logger)
|
|
270
276
|
} catch (error) {
|
|
271
277
|
logger.warn(`could not wrap run: ${messageOf(error)} — using the shipped deadline`)
|
|
272
278
|
return Reflect.apply(original, this, [spec])
|
|
@@ -294,14 +300,14 @@ function patchRun(runtime, settings, logger) {
|
|
|
294
300
|
* @param {unknown} self
|
|
295
301
|
* @param {object} spec
|
|
296
302
|
* @param {number} budget
|
|
297
|
-
* @param {{ humanWaitCapMs: number }}
|
|
303
|
+
* @param {{ enabled: boolean, humanWaitCapMs: number }} live
|
|
298
304
|
* @param {object} logger
|
|
299
305
|
*/
|
|
300
|
-
function runWithPausedDeadline(original, self, spec, budget,
|
|
306
|
+
function runWithPausedDeadline(original, self, spec, budget, live, logger) {
|
|
301
307
|
const controller = new AbortController()
|
|
302
308
|
const outer = spec.signal
|
|
303
309
|
const signal = outer === undefined ? controller.signal : AbortSignal.any([outer, controller.signal])
|
|
304
|
-
const
|
|
310
|
+
const capOf = () => (live.humanWaitCapMs === -1 ? Infinity : live.humanWaitCapMs)
|
|
305
311
|
|
|
306
312
|
let remaining = budget
|
|
307
313
|
let waited = 0
|
|
@@ -344,6 +350,7 @@ function runWithPausedDeadline(original, self, spec, budget, settings, logger) {
|
|
|
344
350
|
if (aborted || finished) return
|
|
345
351
|
settle()
|
|
346
352
|
if (state.waiters > 0) {
|
|
353
|
+
const cap = capOf()
|
|
347
354
|
if (waited >= cap) {
|
|
348
355
|
expire(`human interaction wait exceeded (${cap}ms)`)
|
|
349
356
|
return
|
|
@@ -382,12 +389,38 @@ function runWithPausedDeadline(original, self, spec, budget, settings, logger) {
|
|
|
382
389
|
* @param {object} config
|
|
383
390
|
*/
|
|
384
391
|
export function apply(ctx, config) {
|
|
385
|
-
const settings = normalizeConfig(config)
|
|
386
392
|
const logger = createLogger(ctx)
|
|
393
|
+
// The wrappers read this object on every call, so a settings-page save takes
|
|
394
|
+
// effect without reloading the bundle. `enabled` is not on that page — it is
|
|
395
|
+
// still re-read here, so turning it off through this patch file applies live.
|
|
396
|
+
// `humanWaitCapMs` is the current wait cap.
|
|
397
|
+
const live = normalizeConfig(config)
|
|
398
|
+
|
|
399
|
+
// Own the settings page: the browser half renders the form, so the host must
|
|
400
|
+
// not auto-generate a second one from the Config schema.
|
|
401
|
+
ctx.inject(['settings'], (child) => {
|
|
402
|
+
try {
|
|
403
|
+
child.settings.configure({ auto: false }, ctx.fiber)
|
|
404
|
+
} catch (error) {
|
|
405
|
+
logger.warn(`could not take over the settings page: ${messageOf(error)}`)
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
ctx.effect(() => {
|
|
410
|
+
const timer = setInterval(() => {
|
|
411
|
+
const next = normalizeConfig(config)
|
|
412
|
+
if (next.enabled === live.enabled && next.humanWaitCapMs === live.humanWaitCapMs) return
|
|
413
|
+
live.enabled = next.enabled
|
|
414
|
+
live.humanWaitCapMs = next.humanWaitCapMs
|
|
415
|
+
const cap = live.humanWaitCapMs === -1 ? 'unlimited' : `${live.humanWaitCapMs} ms`
|
|
416
|
+
logger.info(`settings changed — ${live.enabled ? `enabled, wait cap: ${cap}` : 'disabled'}`)
|
|
417
|
+
}, POLL_INTERVAL_MS)
|
|
418
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
419
|
+
return () => clearInterval(timer)
|
|
420
|
+
})
|
|
387
421
|
|
|
388
|
-
if (!
|
|
422
|
+
if (!live.enabled) {
|
|
389
423
|
logger.info('disabled by config — leaving the shipped PTC deadline in place')
|
|
390
|
-
return
|
|
391
424
|
}
|
|
392
425
|
|
|
393
426
|
ctx.inject(['ptcRuntime', 'userQuestions'], (child) => {
|
|
@@ -395,13 +428,13 @@ export function apply(ctx, config) {
|
|
|
395
428
|
const disposers = []
|
|
396
429
|
try {
|
|
397
430
|
for (const key of ['ask', 'askTimed']) {
|
|
398
|
-
const dispose = patchWaitMethod(child.userQuestions, key, logger)
|
|
431
|
+
const dispose = patchWaitMethod(child.userQuestions, key, live, logger)
|
|
399
432
|
if (dispose !== undefined) disposers.push(dispose)
|
|
400
433
|
}
|
|
401
|
-
const dispose = patchRun(child.ptcRuntime,
|
|
434
|
+
const dispose = patchRun(child.ptcRuntime, live, logger)
|
|
402
435
|
if (dispose !== undefined) {
|
|
403
436
|
disposers.push(dispose)
|
|
404
|
-
const cap =
|
|
437
|
+
const cap = live.humanWaitCapMs === -1 ? 'unlimited' : `${live.humanWaitCapMs} ms`
|
|
405
438
|
logger.info(`PTC run deadlines now pause while a human is waiting (wait cap: ${cap})`)
|
|
406
439
|
} else {
|
|
407
440
|
logger.warn('ptcRuntime.run was not patched — human waits will still count against the run deadline')
|
|
@@ -418,6 +451,6 @@ export function apply(ctx, config) {
|
|
|
418
451
|
})
|
|
419
452
|
|
|
420
453
|
ctx.inject(['approval'], (child) => {
|
|
421
|
-
child.effect(() => patchWaitMethod(child.approval, 'request', logger) ?? (() => {}))
|
|
454
|
+
child.effect(() => patchWaitMethod(child.approval, 'request', live, logger) ?? (() => {}))
|
|
422
455
|
})
|
|
423
456
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-human-wait",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Pauses the PTC run_code deadline while DSH is waiting for you to answer a question, review a plan or approve a request, so long human waits are no longer reported as `execution deadline reached (120000ms)`.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -23,10 +23,12 @@
|
|
|
23
23
|
".": "./index.js",
|
|
24
24
|
"./package.json": "./package.json",
|
|
25
25
|
"./locale/*.json": "./locale/*.json",
|
|
26
|
-
"./icon": "./icon.svg"
|
|
26
|
+
"./icon": "./icon.svg",
|
|
27
|
+
"./client": "./client.js"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"index.js",
|
|
31
|
+
"client.js",
|
|
30
32
|
"cordis.patch.yml",
|
|
31
33
|
"locale/*.json",
|
|
32
34
|
"icon.svg",
|
|
@@ -47,6 +49,15 @@
|
|
|
47
49
|
"dsh": {
|
|
48
50
|
"bundle": {
|
|
49
51
|
"patch": "./cordis.patch.yml"
|
|
52
|
+
},
|
|
53
|
+
"client": {
|
|
54
|
+
"platform": "web",
|
|
55
|
+
"immediately": true,
|
|
56
|
+
"inject": [
|
|
57
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
58
|
+
"@deepseek-ai/dsh-client-locale",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
60
|
+
]
|
|
50
61
|
}
|
|
51
62
|
},
|
|
52
63
|
"engines": {
|
|
@@ -57,6 +68,6 @@
|
|
|
57
68
|
"access": "public"
|
|
58
69
|
},
|
|
59
70
|
"scripts": {
|
|
60
|
-
"test": "node --test test/plugin.test.mjs"
|
|
71
|
+
"test": "node --test test/plugin.test.mjs test/client.test.mjs"
|
|
61
72
|
}
|
|
62
73
|
}
|