dsh-human-wait 0.1.1 → 0.2.2

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 — 2026-11-15
4
+
5
+ - Rewrote the README for users: when the plugin helps, what it does and does not change (a table
6
+ with the four cases), how to install and where the setting lives. The Chinese README is now
7
+ primary and `README.en.md` is the English translation.
8
+ - Moved the implementation notes, the acceptance record and the tests to `docs/implementation.md`
9
+ and `docs/implementation.en.md`.
10
+
11
+ ## 0.2.1 — 2026-10-27
12
+
13
+ - Adds a browser half (`client.js`), so the plugin gets its own settings page under **Plugins**:
14
+ a wait-cap field with 1 h / 24 h / unlimited quick values, an "overridden" chip and a
15
+ "restore default" action.
16
+ - `enabled` and `humanWaitCapMs` are now volatile: they are re-read while the plugin runs, so a
17
+ save applies in about 1.5 s without restarting DSH. The wrappers also check `enabled` on
18
+ every call, so turning the plugin off through the patch takes effect immediately.
19
+ - The host half takes over the settings page (`settings.configure({ auto: false })`) instead of
20
+ letting DSH auto-generate a second, schema-only one.
21
+ - Tests: adds `test/client.test.mjs` (bundle id, slot key, page and summary rendering) and two
22
+ runtime cases for re-reading the settings.
23
+
3
24
  ## 0.1.1 — 2026-10-26
4
25
 
5
26
  First release published by GitHub Actions with npm provenance (trusted publishing).
package/README.en.md ADDED
@@ -0,0 +1,69 @@
1
+ # dsh-human-wait
2
+
3
+ **Stops the PTC `run_code` deadline while DSH is waiting for you.**
4
+
5
+ PTC mode wraps the whole program — including any question, plan review or approval raised mid-run — in one 120-second wall clock. Think a little too long and the run is already gone:
6
+
7
+ ```
8
+ Error: code run failed (timeout): execution deadline reached (120000ms)
9
+ ```
10
+
11
+ and the question card is reported as **interrupted**. With this plugin, the time you spend answering is no longer charged to that budget; a runaway program is still cut off when the budget runs out.
12
+
13
+ > English | [中文](README.md)
14
+
15
+ ## When you need it
16
+
17
+ - You run programs in PTC mode that ask you questions or need approvals, and you want to read before answering
18
+ - You have seen a question card flip to "interrupted", or the timeout above
19
+
20
+ ## What it changes, and what it does not
21
+
22
+ | Situation | Without it | With it |
23
+ | --- | --- | --- |
24
+ | A question inside the run, answered in 20 s | fine | fine |
25
+ | Same run, answered after 4 minutes | aborted at 120 s, question shown as interrupted | keeps waiting, the answer is delivered |
26
+ | Never answered | aborted at 120 s | aborted once the wait hits the cap (24 h by default) |
27
+ | A program that spins for 10 minutes without asking anything | aborted at 120 s | aborted at 120 s — unchanged |
28
+
29
+ ## Install
30
+
31
+ 1. Open **Settings → Plugins → Market** and search for `dsh-human-wait`
32
+ 2. Install it
33
+ 3. **Quit DSH completely, then start it again**
34
+
35
+ ## Settings
36
+
37
+ **Settings → Plugins → Installed → Human wait**:
38
+
39
+ - **Longest wait** (24 hours by default; "unlimited" removes the cap)
40
+ - Quick buttons: 1 hour / 24 hours / unlimited
41
+ - **Restore default** appears once the field differs from the shipped value
42
+ - A one-line summary sits on the card
43
+
44
+ A change takes effect in about 1.5 seconds, no restart.
45
+
46
+ ## FAQ
47
+
48
+ **It still timed out?** Check three things: the session header shows the `PTC` badge; the plugin's `enabled` is not set to false in the profile patch; the log has a line starting with `[dsh-human-wait]`. If the wrapper did not see that wait, it cannot pause for it.
49
+
50
+ **The error kind became `abort`?** Expected. The plugin aborts through its own signal, so the runtime classifies it as `abort` instead of `timeout`; the text still contains `execution deadline reached`.
51
+
52
+ **Does it affect standard sessions?** No. Standard sessions never had this timeout — they wait indefinitely by design.
53
+
54
+ ## Privacy and cost
55
+
56
+ - Wraps three host service calls in memory only; writes no files and opens no network connections
57
+ - Does not modify the conversation, and does not change the program's own execution budget
58
+
59
+ ## Uninstall
60
+
61
+ Disable or remove it in the plugin panel, then restart DSH; the host's original methods come back.
62
+
63
+ ## Technical notes
64
+
65
+ How it works, the two settings, the fail-open behaviour and the acceptance record: [docs/implementation.en.md](https://github.com/SpaceRelay/dsh-subagent-control/blob/main/human-wait/docs/implementation.en.md)
66
+
67
+ ## License
68
+
69
+ MIT — see [LICENSE](./LICENSE). Issues: [github.com/SpaceRelay/dsh-subagent-control/issues](https://github.com/SpaceRelay/dsh-subagent-control/issues)
package/README.md CHANGED
@@ -1,166 +1,69 @@
1
1
  # dsh-human-wait
2
2
 
3
- **Stops the PTC `run_code` deadline while DSH is waiting for you.** In PTC mode a question
4
- (`ask_user_question`), the plan review raised by `exit_plan_mode`, or a sandbox approval is a
5
- nested call inside the one `run_code` program — and `dsh-ptc-runtime-node` measures a single
6
- wall-clock budget across the whole program, *including the time you spend answering*. Wait
7
- longer than what is left of that budget and the run dies with:
3
+ **PTC 模式下,DSH 在等你回答问题时,不再让执行超时继续倒数。**
4
+
5
+ PTC(`run_code`)把整个程序——包括中途弹出的问题、计划审批、权限批准——包在一条 120 秒的墙钟预算里。你想久一点再回答,程序就已经超时了:
8
6
 
9
7
  ```
10
8
  Error: code run failed (timeout): execution deadline reached (120000ms)
11
9
  ```
12
10
 
13
- and the question card is reported as **已中断 / interrupted**. This plugin keeps the deadline
14
- for execution but pauses it while a human-interaction wait is pending. A runaway program is
15
- still cut off after its budget; a question you think about for an hour is not.
16
-
17
- > 中文摘要:PTC 模式下提问、计划审批和批准都是 `run_code` 里的嵌套调用,共用同一条 120 秒墙钟预算,
18
- > 所以「想久一点再回答」会让你看到的是一次 `execution deadline reached (120000ms)` 超时。本插件
19
- > 保留执行的截止时间,但在**等待你回答/批准期间暂停计时**;等待本身另有上限(默认 24 小时,可设为无限)。
20
-
21
- ---
22
-
23
- ## What it does
24
-
25
- | Situation | Without the plugin | With the plugin |
26
- | --- | --- | --- |
27
- | A question inside `run_code`, you answer in 20 s, run budget was 120 s | OK | OK |
28
- | Same run, you answer after 4 minutes | run aborted at 120 s, question interrupted | run keeps waiting; the answer is delivered |
29
- | Same run, you never answer | run aborted at 120 s | run aborted once the wait exceeds `humanWaitCapMs` (24 h) |
30
- | A program that spins for 10 minutes without asking anything | aborted at 120 s | aborted at 120 s — unchanged |
31
-
32
- It works by:
33
-
34
- 1. wrapping `ptcRuntime.run` and moving the deadline into the plugin, so it can be paused;
35
- 2. wrapping `userQuestions.ask` / `userQuestions.askTimed` and `approval.request` — the three
36
- service calls that exist only because a human is being waited on;
37
- 3. charging the budget only while no such wait is pending (`node:async_hooks`
38
- `AsyncLocalStorage` keeps the accounting scoped to the run that is actually waiting).
11
+ 问题卡还会被标成「已中断」。装上这个插件,等待你回答的那段时间不再计入预算;程序自己跑飞了,照样在预算到点时被掐断。
39
12
 
40
- A single `run_code` call can still pass its own `timeoutMs`; that number keeps working as the
41
- execution budget and is what the plugin enforces.
13
+ > [English](README.en.md) | 中文
42
14
 
43
- ## Install
15
+ ## 什么时候用得上
44
16
 
45
- The plugin is a normal DSH bundle (one inserted host row, no client half). Pick one route:
17
+ - 用 PTC 模式跑程序,中途要你回答问题或审批,而你想读完再答
18
+ - 遇到过「问题卡突然变成已中断」或者上面那条超时
46
19
 
47
- **A. From the registry (recommended).** Use the Plugins settings page, or a
48
- `plugin_manager install_bundle` call in a session, with the spec `dsh-human-wait@0.1.1`; the
49
- plugin manager adds both the dependency and the bundle entry for you.
20
+ ## 它改什么、不改什么
50
21
 
51
- **B. From a tarball, by hand.** In `<DSH_HOME>/profiles/<profile>/package.json` add the dependency
52
- and the bundle name, then install it:
22
+ | 情况 | 没装 | 装了 |
23
+ | --- | --- | --- |
24
+ | 程序里提问,20 秒答完 | 正常 | 正常 |
25
+ | 同一个程序,4 分钟后才答 | 120 秒就被中止,问题卡显示已中断 | 继续等,答案照常送达 |
26
+ | 一直不答 | 120 秒中止 | 等到最长等待上限(默认 24 小时)才中止 |
27
+ | 程序自己死循环 10 分钟 | 120 秒中止 | 120 秒中止,行为不变 |
53
28
 
54
- ```jsonc
55
- {
56
- "dependencies": {
57
- "dsh-human-wait": "file:D:/path/to/dsh-human-wait-0.1.1.tgz"
58
- },
59
- "dsh": { "profile": { "bundles": ["dsh-human-wait"] } }
60
- }
61
- ```
29
+ ## 安装
62
30
 
63
- ```powershell
64
- # pnpm ships with the desktop runtime
65
- & "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\pnpm.cjs" install --dir "$env:USERPROFILE\.dsh\profiles\desktop"
66
- ```
31
+ 1. 打开 设置 → 插件 → 市场,搜索 `dsh-human-wait`
32
+ 2. 点安装
33
+ 3. **完全退出 DSH,再重新打开**
67
34
 
68
- Then **quit DSH from the tray icon and start it again** — bundles are composed at boot. The
69
- Electron-managed profile cannot be booted through the `dsh` CLI (`profile "desktop" is managed
70
- exclusively by the Electron application`), so restarting the app is the only way to apply it.
71
- Removing the bundle name from `dsh.profile.bundles` and deleting the dependency disables the
72
- plugin again; the wrappers are also restored at unload time.
35
+ ## 设置
73
36
 
74
- ## Configuration
37
+ 设置 → 插件 → 已安装 → **人机等待**:
75
38
 
76
- The row ships sensible defaults; override them in the same profile's `cordis.patch.yml` when
77
- you want something else:
39
+ - **最长等待**(默认 24 小时;选「无限」表示不设上限)
40
+ - 快捷按钮:1 小时 / 24 小时 / 无限
41
+ - 字段偏离出厂值时会出现「恢复默认」
42
+ - 卡片上有一行摘要
78
43
 
79
- ```yaml
80
- - id: human-wait
81
- name: 'dsh-human-wait'
82
- config:
83
- enabled: true
84
- humanWaitCapMs: 86400000 # 24 h; -1 = no cap at all
85
- ```
44
+ 改完约 1.5 秒生效,不用重启。
86
45
 
87
- | Option | Default | Meaning |
88
- | --- | --- | --- |
89
- | `enabled` | `true` | Turn the pause on/off. `false` reproduces the shipped behaviour exactly. |
90
- | `humanWaitCapMs` | `86400000` | Longest time one run may spend waiting for a human. `-1` removes the cap. |
91
-
92
- How the clock works: the plugin re-arms a single timer on every accounting event (run start, a
93
- wait starting or ending, or the timer itself firing), so there is no fixed sampling period and
94
- no configuration knob for one.
95
-
96
- Changes are read when the plugin loads, so restart DSH after editing.
97
-
98
- ## What counts as a human wait
99
-
100
- - `userQuestions.ask` — blocking questions, including the `plan-review` question from
101
- `exit_plan_mode`;
102
- - `userQuestions.askTimed` — timed questions;
103
- - `approval.request` — sandbox/policy approvals that block on the user.
104
-
105
- Tool calls that finish on their own (bash, file reads, web fetches, subagents) are **not**
106
- waits: their time is still charged to the run, exactly as before.
107
-
108
- ## Behaviour notes
109
-
110
- - **Fail-open.** If `ptcRuntime` / `userQuestions` / `approval` are missing, if
111
- `AbortSignal.any` is unavailable, or if a wrapper cannot be installed, the plugin logs one
112
- warning and leaves the shipped deadline untouched.
113
- - **Restored on unload.** Every wrapper is removed when the plugin unloads (or is disabled and
114
- the profile restarted); the original methods are back.
115
- - **The terminal error changes kind.** When the paused execution budget runs out, the runtime
116
- reports `kind: "abort"` instead of `kind: "timeout"`, because the plugin aborts the run
117
- through its signal. The message still contains `execution deadline reached (<budget>ms)`, so
118
- the surfaced text is the same.
119
- - **Only PTC sessions are affected.** Standard sessions never had this timeout: their
120
- questions wait indefinitely by design.
121
- - **Not a public extension point.** The plugin patches methods of two host services
122
- (`ptcRuntime`, `userQuestions`) and one approval service. That is the only way to implement
123
- this today; a future DSH release may change those internals and make the patch a no-op. The
124
- plugin is written so that this failure mode is safe, not fatal.
125
-
126
- Verified against DSH desktop **0.2.1-rc.3** (`dsh-ptc-runtime-node` 0.2.1, node 24.21).
127
- `ptc-runtime-node/lib/index.js` is where the shipped timer lives:
128
-
129
- ```js
130
- const wallTimer = spec.timeoutMs === null ? void 0 : setTimeout(() => {
131
- timedOut = true;
132
- controller.abort("execution deadline reached");
133
- }, spec.timeoutMs);
134
- ```
46
+ ## 常见问题
135
47
 
136
- ## Troubleshooting
48
+ **还是超时了?** 确认三件事:会话头部显示 PTC 标记;插件的 `enabled` 没在 profile 补丁里被关掉;日志里有 `[dsh-human-wait]` 开头的那行说明。如果等待期间恰好插件没看到这次等待,它就不会暂停计时。
137
49
 
138
- - Look for `[dsh-human-wait]` lines in the DSH log; the plugin logs either
139
- `PTC run deadlines now pause while a human is waiting (wait cap: …)` or the reason it stayed
140
- out of the way.
141
- - If a wait still dies with `execution deadline reached`, the wrapper did not see the wait.
142
- Check that PTC mode is actually in use (the session header shows the `PTC` badge) and that
143
- `enabled` is not `false`.
50
+ **错误类型变成 `abort` 了?** 正常。插件用自己的信号中止程序,所以 kind 从 `timeout` 变成 `abort`,提示文字里仍然有 `execution deadline reached`。
144
51
 
145
- ## Development
52
+ **普通会话受影响吗?** 不受影响。普通会话本来就会一直等你回答,没有这个超时。
146
53
 
147
- ```powershell
148
- npm test # node --test, no dependencies
149
- ```
54
+ ## 隐私与开销
150
55
 
151
- ## License
56
+ - 只在内存里包住三个宿主服务的调用,不写文件、不联网
57
+ - 不修改会话内容,也不改变程序本身的执行预算
152
58
 
153
- MIT
59
+ ## 卸载
154
60
 
155
- ## Acceptance record
61
+ 在插件面板里停用或卸载,然后重启 DSH;宿主原来的方法会恢复。
156
62
 
157
- Recorded 2026-10-26 against `dsh-human-wait` 0.1.0 in the desktop profile (PTC mode, shipped 120 s budget):
63
+ ## 技术细节
158
64
 
159
- | Test | Scenario | Observed |
160
- | --- | --- | --- |
161
- | A | nested `ask_user_question`, answered after 294037 ms | answer delivered, run continued, no `execution deadline reached` |
162
- | B | plan review submitted through `exit_plan_mode`, approved after 313073 ms | plan approved, run continued, no automatic cancellation at 120 s |
163
- | C | `run_code` running `while (true) {}` | killed after ~120 s with `Error: code run failed (abort): execution deadline reached (120000ms)` |
65
+ 实现原理、两个配置项、失败时的降级行为与验收记录:[docs/implementation.md](https://github.com/SpaceRelay/dsh-subagent-control/blob/main/human-wait/docs/implementation.md)
164
66
 
165
- Test C's `(abort)` label is expected — the plugin aborts through its own signal, so the runtime classifies the failure as `abort` instead of `timeout` while the message text stays `execution deadline reached (120000ms)`.
67
+ ## 许可
166
68
 
69
+ MIT,见 [LICENSE](./LICENSE)。问题反馈:[github.com/SpaceRelay/dsh-subagent-control/issues](https://github.com/SpaceRelay/dsh-subagent-control/issues)
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'
@@ -0,0 +1,77 @@
1
+ # dsh-human-wait — implementation notes
2
+
3
+ For maintainers. User-facing docs live in [../README.en.md](../README.en.md).
4
+
5
+ ## The problem
6
+
7
+ In PTC mode, `dsh-ptc-runtime-node` puts one wall-clock budget around the entire `run_code` program, nested tool calls and approval waits included:
8
+
9
+ ```js
10
+ const wallTimer = spec.timeoutMs === null ? void 0 : setTimeout(() => {
11
+ timedOut = true
12
+ controller.abort("execution deadline reached")
13
+ }, spec.timeoutMs)
14
+ ```
15
+
16
+ So the time spent waiting for you burns the budget too. This project takes the timer over and charges it only while no human-interaction wait is pending.
17
+
18
+ ## How it works
19
+
20
+ 1. Wrap `ptcRuntime.run`: the deadline moves into the plugin, which can pause it.
21
+ 2. Wrap the three points where a human is actually being waited on: `userQuestions.ask`, `userQuestions.askTimed`, `approval.request`.
22
+ 3. Use `node:async_hooks` `AsyncLocalStorage` to scope the accounting to the run that is really waiting, charging the budget only while no such wait is pending.
23
+
24
+ A single `run_code` call can still pass its own `timeoutMs`; that number keeps working as the execution budget.
25
+
26
+ ## Configuration
27
+
28
+ The row ships sensible defaults; override them in the same profile's `cordis.patch.yml`:
29
+
30
+ ```yaml
31
+ - id: human-wait
32
+ name: 'dsh-human-wait'
33
+ config:
34
+ enabled: true
35
+ humanWaitCapMs: 86400000 # 24 h; -1 = no cap at all
36
+ ```
37
+
38
+ | Option | Default | Meaning |
39
+ | --- | --- | --- |
40
+ | `enabled` | `true` | Master switch for pausing. `false` reproduces the shipped behaviour exactly. |
41
+ | `humanWaitCapMs` | `86400000` | Longest time one run may spend waiting for a human. `-1` removes the cap. |
42
+
43
+ Both options are **volatile**: they are re-read while the plugin runs, so a change applies in about 1.5 seconds without a restart. A hand-edited patch is read at boot.
44
+
45
+ ## Settings page
46
+
47
+ `client.js` is a hand-written browser half registered on the `plugins.bundle.config` slot (key `dsh-human-wait`): a `humanWaitCapMs` number field, 1 hour / 24 hours / unlimited quick buttons, a field-level "overridden" chip with "restore default", and a one-line summary on the card. `enabled` stays in the patch layer.
48
+
49
+ ## Behaviour notes
50
+
51
+ - **Fail-open.** If `ptcRuntime` / `userQuestions` / `approval` are missing, if `AbortSignal.any` is unavailable, or if a wrapper cannot be installed, the plugin logs one warning and leaves the shipped deadline untouched.
52
+ - **Restored on unload.** Every wrapper is removed when the plugin unloads (or is disabled and the profile restarted); the original methods are back.
53
+ - **The terminal error changes kind.** When the paused execution budget runs out, the runtime reports `kind: "abort"` instead of `kind: "timeout"`, because the plugin aborts the run through its signal. The message still contains `execution deadline reached (<budget>ms)`.
54
+ - **Only PTC sessions are affected.** Standard sessions never had this timeout: their questions wait indefinitely by design.
55
+ - **Not a public extension point.** The plugin patches methods of two host services (`ptcRuntime`, `userQuestions`) and one approval service. That is the only way to implement this today; a future DSH release may change those internals and make the patch a no-op. The plugin is written so that this failure mode is safe, not fatal.
56
+
57
+ ## Verified environment and acceptance record
58
+
59
+ Verified against DSH desktop **0.2.1-rc.3** (`dsh-ptc-runtime-node` 0.2.1, Node 24.21).
60
+
61
+ Recorded 2026-10-26 against `dsh-human-wait` 0.1.0 in the desktop profile (PTC mode, shipped 120 s budget):
62
+
63
+ | Test | Scenario | Observed |
64
+ | --- | --- | --- |
65
+ | A | nested `ask_user_question`, answered after 294037 ms | answer delivered, run continued, no `execution deadline reached` |
66
+ | B | plan review submitted through `exit_plan_mode`, approved after 313073 ms | plan approved, run continued, no automatic cancellation at 120 s |
67
+ | C | `run_code` running `while (true) {}` | killed after ~120 s with `Error: code run failed (abort): execution deadline reached (120000ms)` |
68
+
69
+ Test C's `(abort)` label is expected — the plugin aborts through its own signal, so the runtime classifies the failure as `abort` instead of `timeout` while the message text stays `execution deadline reached (120000ms)`.
70
+
71
+ ## Tests
72
+
73
+ ```powershell
74
+ npm test # node --test, no dependencies
75
+ ```
76
+
77
+ Coverage: re-reading both settings, wrapping and restoring the three wait points, the fail-open branches, and the browser half (bundle id, slot key, page rendering).
@@ -0,0 +1,77 @@
1
+ # dsh-human-wait 实现说明
2
+
3
+ 面向维护者。用户向说明见 [../README.md](../README.md)。
4
+
5
+ ## 问题
6
+
7
+ PTC 模式下,`dsh-ptc-runtime-node` 用一条墙钟预算覆盖整个 `run_code` 程序,嵌套的工具调用与审批等待都算在里面:
8
+
9
+ ```js
10
+ const wallTimer = spec.timeoutMs === null ? void 0 : setTimeout(() => {
11
+ timedOut = true
12
+ controller.abort("execution deadline reached")
13
+ }, spec.timeoutMs)
14
+ ```
15
+
16
+ 于是「等你回答问题」也烧预算。本项目把这个计时器接管过来,只在没有人类交互等待时计费。
17
+
18
+ ## 做法
19
+
20
+ 1. 包装 `ptcRuntime.run`:把 deadline 移进插件,由插件自己计时并可暂停。
21
+ 2. 包装三个人类等待点:`userQuestions.ask`、`userQuestions.askTimed`、`approval.request`。只有这三类调用会产生「等待人」的时间。
22
+ 3. 用 `node:async_hooks` 的 `AsyncLocalStorage` 把计费限定在真正在等待的那次运行上,只在没有等待挂起时扣预算。
23
+
24
+ 单个 `run_code` 调用仍可自带 `timeoutMs`,它继续作为执行预算生效。
25
+
26
+ ## 配置
27
+
28
+ 行内默认值够用;要改就在 profile 的 `cordis.patch.yml` 里覆盖:
29
+
30
+ ```yaml
31
+ - id: human-wait
32
+ name: 'dsh-human-wait'
33
+ config:
34
+ enabled: true
35
+ humanWaitCapMs: 86400000 # 24 小时;-1 = 不设上限
36
+ ```
37
+
38
+ | 选项 | 默认 | 含义 |
39
+ | --- | --- | --- |
40
+ | `enabled` | `true` | 暂停总开关。`false` 完全回到宿主原生行为 |
41
+ | `humanWaitCapMs` | `86400000` | 单次运行等待人类的最长时间,`-1` 表示不限 |
42
+
43
+ 两个选项都是 **volatile**:插件运行期间会被重新读取,改完约 1.5 秒生效,不用重启。手动改补丁则在下次启动时读取。
44
+
45
+ ## 设置页
46
+
47
+ `client.js` 是手写的浏览器半边,注册在 `plugins.bundle.config` 槽(key `dsh-human-wait`):一个 `humanWaitCapMs` 数字框、1 小时 / 24 小时 / 无限三个快捷按钮、字段级「已覆盖」与「恢复默认」,卡片上还有一行摘要。`enabled` 留在补丁层。
48
+
49
+ ## 行为说明
50
+
51
+ - **Fail-open。** 缺少 `ptcRuntime` / `userQuestions` / `approval`、没有 `AbortSignal.any`、或者包装装不上时,插件只打一条警告,宿主原有 deadline 原样保留。
52
+ - **卸载时恢复。** 插件卸载(或被停用后重启)时每个包装都会被拆掉,宿主原方法恢复。
53
+ - **错误 kind 会变。** 暂停后的执行预算耗尽时,运行时报告 `kind: "abort"` 而不是 `"timeout"`,因为插件是通过自己的信号中止的;消息文本里仍有 `execution deadline reached (<budget>ms)`。
54
+ - **只影响 PTC 会话。** 标准会话本来就没有这个超时。
55
+ - **不是公开扩展点。** 插件包装的是两个宿主服务(`ptcRuntime`、`userQuestions`)和审批服务的方法,这是当前唯一可行的实现方式;未来 DSH 版本改动这些内部实现可能让它变成空操作——代码写成这种失败是安全的,不是致命的。
56
+
57
+ ## 验证环境与验收记录
58
+
59
+ 验证环境:DSH desktop **0.2.1-rc.3**(`dsh-ptc-runtime-node` 0.2.1,Node 24.21)。
60
+
61
+ 2026-10-26 在 `dsh-human-wait` 0.1.0 上测得(桌面 profile,PTC 模式,出厂 120 秒预算):
62
+
63
+ | 用例 | 场景 | 结果 |
64
+ | --- | --- | --- |
65
+ | A | 嵌套 `ask_user_question`,294037 ms 后才回答 | 答案送达,运行继续,没有 `execution deadline reached` |
66
+ | B | `exit_plan_mode` 计划审批,313073 ms 后才批准 | 计划通过,运行继续,没有被 120 秒取消 |
67
+ | C | `run_code` 里 `while (true) {}` | 约 120 秒后被终止,`Error: code run failed (abort): execution deadline reached (120000ms)` |
68
+
69
+ 用例 C 的 `(abort)` 是预期行为:插件用自己的信号中止,运行时因此把失败归类为 `abort` 而不是 `timeout`,消息文本不变。
70
+
71
+ ## 测试
72
+
73
+ ```powershell
74
+ npm test # node --test,无依赖
75
+ ```
76
+
77
+ 覆盖:两个配置项的重读、三个等待点的包装与恢复、fail-open 分支,以及浏览器半边的 bundle id / 槽位 / 页面渲染。
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 }} settings
254
+ * @param {{ enabled: boolean, humanWaitCapMs: number }} live
250
255
  * @param {object} logger
251
256
  */
252
- function patchRun(runtime, settings, logger) {
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, settings, logger)
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 }} settings
303
+ * @param {{ enabled: boolean, humanWaitCapMs: number }} live
298
304
  * @param {object} logger
299
305
  */
300
- function runWithPausedDeadline(original, self, spec, budget, settings, logger) {
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 cap = settings.humanWaitCapMs === -1 ? Infinity : settings.humanWaitCapMs
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 (!settings.enabled) {
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, settings, logger)
434
+ const dispose = patchRun(child.ptcRuntime, live, logger)
402
435
  if (dispose !== undefined) {
403
436
  disposers.push(dispose)
404
- const cap = settings.humanWaitCapMs === -1 ? 'unlimited' : `${settings.humanWaitCapMs} ms`
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.1",
3
+ "version": "0.2.2",
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,14 +23,18 @@
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",
33
35
  "README.md",
36
+ "README.en.md",
37
+ "docs",
34
38
  "CHANGELOG.md",
35
39
  "LICENSE"
36
40
  ],
@@ -47,6 +51,15 @@
47
51
  "dsh": {
48
52
  "bundle": {
49
53
  "patch": "./cordis.patch.yml"
54
+ },
55
+ "client": {
56
+ "platform": "web",
57
+ "immediately": true,
58
+ "inject": [
59
+ "@deepseek-ai/dsh-api-remotes",
60
+ "@deepseek-ai/dsh-client-locale",
61
+ "@deepseek-ai/dsh-client-ui-settings"
62
+ ]
50
63
  }
51
64
  },
52
65
  "engines": {
@@ -57,6 +70,6 @@
57
70
  "access": "public"
58
71
  },
59
72
  "scripts": {
60
- "test": "node --test test/plugin.test.mjs"
73
+ "test": "node --test test/plugin.test.mjs test/client.test.mjs"
61
74
  }
62
75
  }