dsh-transfer-hud 1.0.0 → 1.1.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/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  English | [中文](#中文)
4
4
 
5
- A thin strip below the message composer that shows what DeepSeek Harness is downloading
6
- right now. Zero runtime dependencies; a Host half (`index.js`) plus a Web Client half
7
- (`client.js`). Also stops (background) downloads from the strip.
5
+ A thin strip below the message composer that shows what DeepSeek Harness is doing
6
+ **right now** — downloads, installs, builds, model loads and any long command. Zero
7
+ runtime dependencies; a Host half (`index.js`) plus a Web client half (`client.js`). The
8
+ strip is also where background jobs and `run_with_progress` commands are stopped.
8
9
 
9
- > **Version:** this npm release is **1.0.0** and packages the workspace HUD revision
10
- > **v1.1.1** (refined download classifier). The first public release renumbers the
11
- > internal revision to 1.0.0.
10
+ > **Version:** this npm release is **1.1.0** and packages the workspace task-progress HUD
11
+ > revision **v1.3.0** (loads and long tasks, the `run_with_progress` tool, cross-platform
12
+ > process handling). The first public release renumbered the internal revision to 1.0.0;
13
+ > 1.1.0 is the matching npm version for workspace v1.3.0.
12
14
 
13
15
  ## English
14
16
 
@@ -16,12 +18,13 @@ right now. Zero runtime dependencies; a Host half (`index.js`) plus a Web Client
16
18
 
17
19
  | Source | Row | Progress |
18
20
  |---|---|---|
19
- | **Background jobs** (`ctx.jobs`: `pwsh`/`bash` jobs started with `run_in_background`, or promoted after a timeout) | real job row: label, status, **Stop** button | percent / bytes / rate / ETA when the output ring carries a meter (curl, wget, aria2, tqdm / `hf download`, huggingface, modelscope, `git` counters, …), parsed from **stdout and stderr** — most downloaders print their meter to stderr |
20
- | **Foreground shell commands** (`pwsh`, `bash`, `ops_bash`, `terminal_send`, …) | indeterminate row (`foreground: true`, no Stop button) | none — the Host only sees the call, not its live output; the row proves a download is running and disappears shortly after the tool result |
21
+ | **Background jobs** (`ctx.jobs`: `pwsh`/`bash` jobs started with `run_in_background`, or promoted after a timeout) | real job row: label, status, a `download` / `load` badge (`下载` / `加载` in the Chinese UI), **Stop** button | percent / bytes / rate / ETA whenever the output ring carries a meter (curl, wget, aria2, tqdm / `hf download`, huggingface, modelscope, `git` counters, npm/pip/apt, ffmpeg, docker, `step 120/500`, `[=====> ]`, …), parsed from **stdout and stderr** — most tools print their meter to stderr |
22
+ | **Foreground shell commands** (`pwsh`, `bash`, `ops_bash`, `terminal_send`, …) | indeterminate row (`foreground: true`, no Stop button) when the call looks like a download/load, or once it has been running for more than 12 s | none — the Host sees the call, not its live output; the row proves work is running and disappears about 8 s after the tool result |
23
+ | **`run_with_progress` model tool** | real row (`kind: 'tool'`, **Stop** button while running) | **live** percent / bytes / rate / ETA parsed from the command's merged stdout+stderr, because the plugin owns the child process; the row lingers 30 s after the command ends |
21
24
 
22
- When a foreground call starts a background job (`run_in_background: true`, or a
23
- foreground command promoted on timeout), the job row replaces the foreground row: the
24
- Host reads `job/registered`'s `toolCall.callId` and dedupes.
25
+ When a foreground call starts a background job (`run_in_background: true`, or a foreground
26
+ command promoted on timeout), the job row replaces the foreground row: the Host reads
27
+ `job/registered`'s `toolCall.callId` and dedupes.
25
28
 
26
29
  ### What counts as a download
27
30
 
@@ -47,70 +50,177 @@ The classifier is deliberately narrow so ordinary API traffic never shows a row:
47
50
  are ignored. An opaque call such as `python fetch.py` is recognised only when the call's
48
51
  description says "download/fetch/…" or its file/URL matches a known artifact pattern.
49
52
 
53
+ ### What counts as a load
54
+
55
+ A load is everything else that is clearly working rather than answering. Three signals
56
+ admit a row:
57
+
58
+ - **Any progress meter in a job's output** — a percent, a `done/total` byte pair, a
59
+ rendered bar (`[====> ]`), a tqdm iteration rate (`it/s`), a step / epoch / phase
60
+ counter. A meter alone makes it a load row, even without a single download word.
61
+ - **Loader commands** in a job label or in command position: `npm`/`pnpm`/`yarn`
62
+ `install|ci|add`, `apt(-get) install`, `dnf`/`yum`/`zypper install`, `pacman -S`,
63
+ `snap`/`flatpak install`, `dpkg -i`, `rpm -i`, `brew install`, `conda install`,
64
+ `pip install`, `docker pull|build`, `ffmpeg`, `cargo`/`go build`, `make`/`cmake`,
65
+ `dotnet build|restore`, `ollama pull`, macOS `installer`/`pkgutil`, and load verbs such
66
+ as 加载 / 构建 / 编译 / 安装 / 渲染 / 转码 / 导出 / 训练 / 解压.
67
+ - **Long foreground calls without any hint** are promoted to an indeterminate load row
68
+ after 12 s.
69
+
70
+ Reads and searches are excluded: a job or command whose command word is
71
+ `cat`/`Get-Content`/`grep`/`rg`/`find`/`ls`/`dir` does not become a load just because its
72
+ output happens to contain `100%` or `eta`. A description alone is not enough either, so
73
+ `Check build status` running `git status` stays silent. Background-job **labels** are
74
+ scanned with the broader historical hint (the whole multi-line script): a background job
75
+ is rarer and a missed load costs more than an extra strip row.
76
+
77
+ A download keeps its `下载` badge only when a download-specific signal — the label, the
78
+ tail text, or the foreground classifier — says so; a tqdm training run is a load, not a
79
+ download.
80
+
81
+ ### The `run_with_progress` tool
82
+
83
+ The Host half registers a model tool, `run_with_progress`, and injects a short global
84
+ system-prompt hint that tells the model to use it for long operations. Because the plugin
85
+ owns the child process, a command run through this tool streams a real percent / rate /
86
+ ETA into the strip even though ordinary foreground shell calls expose no progress to the
87
+ Host.
88
+
89
+ ```json
90
+ {
91
+ "command": "curl -# -L -o model.safetensors https://example.com/model.safetensors",
92
+ "label": "Downloading model weights"
93
+ }
94
+ ```
95
+
96
+ - **When to use it.** Downloads, installs, builds, model loads, transcoding, dataset
97
+ processing and any command expected to run longer than about 10 seconds. Both the tool
98
+ description and the global prompt prefer this tool over a plain `ops_bash` / `pwsh`
99
+ call for those tasks.
100
+ - **Arguments.** `command` (required string — one shell command line) and `label`
101
+ (optional short label for the strip).
102
+ - **Where it runs.** In the calling session's working directory, so relative paths
103
+ resolve against the session's workspace; `stdout` and `stderr` are merged and parsed as
104
+ they arrive, and the row is served on `/dsh-download-hud/state` like any other row.
105
+ - **Concurrency.** Up to 4 commands run at once (other sessions included); a fifth call
106
+ returns a `Busy` text result and adds no row.
107
+ - **Cancel.** The user can press **Stop** on the row: the Host kills the whole process
108
+ tree, the row becomes `killed`, and the tool returns `status: killed`. Cancelling the
109
+ tool call (turn abort) takes the same path.
110
+ - **Result.** `status`, `exit code`, `duration` and the command output. Output is returned
111
+ whole up to 12 KB; beyond that the first 2 KB and last 10 KB are kept, separated by a
112
+ `[N characters truncated]` marker.
113
+ - **Keep the meter.** The prompt asks for `curl -#` / `--progress-bar` and never `-s`,
114
+ default progress output for `wget`/`pip`/`npm`/`hf`/`aria2`, `ffmpeg -progress pipe:1
115
+ -nostats`, and no `$ProgressPreference = 'SilentlyContinue'` in PowerShell.
116
+ - **Degrades gracefully.** Hosts without a `tools` / `systemPrompt` service still load the
117
+ plugin and keep the strip; only the tool registration or the prompt hint is skipped.
118
+
50
119
  ### Install
51
120
 
52
121
  Requires DeepSeek Harness `>=0.2.1-rc.2 <0.3.0-0` and a Web or Desktop profile
53
122
  (the plugin has a Host half and a browser Client half).
54
123
 
55
- 1. Open **Settings → Plugins**, install bundle `dsh-transfer-hud@1.0.0`.
124
+ 1. Open **Settings → Plugins**, install bundle `dsh-transfer-hud@1.1.0`.
56
125
  2. Or ask the agent: `plugin_manager` → `action: "install_bundle"`,
57
- `target: "dsh-transfer-hud@1.0.0"`.
126
+ `target: "dsh-transfer-hud@1.1.0"`.
58
127
 
59
128
  **Restart the app after installing or upgrading.** HMR applies a new bundle, but the
60
129
  profile's Host watch roots are empty (`root: []`) and a replaced package needs a fresh
61
130
  JavaScript module generation, so the new Host code is loaded on restart; the Client half
62
131
  then follows with the usual client HMR / page refresh. Verification: after the restart,
63
- run a foreground download (e.g. `curl -o file.bin <url>`) or a background job
64
- (`run_in_background: true`) and the strip appears below the composer. Remove the bundle to
65
- uninstall.
132
+ run a foreground download (e.g. `curl -o file.bin <url>`), a background job
133
+ (`run_in_background: true`), or a long command through `run_with_progress` and the strip
134
+ appears below the composer. Remove the bundle to uninstall.
66
135
 
67
136
  ### Configuration
68
137
 
69
138
  None. The row takes no config; the strip is automatic and only reads activity that
70
139
  already belongs to this Harness.
71
140
 
141
+ ### Cross-platform
142
+
143
+ The HUD and the `run_with_progress` runner are platform-neutral and ship no native code —
144
+ they use Node's `child_process` plus the platform's own process-control command.
145
+
146
+ - **Windows, macOS, Linux.** Commands run through the platform's default shell
147
+ (`ComSpec` on Windows, `/bin/sh` on POSIX), so ordinary portable command lines work
148
+ unchanged.
149
+ - **Cancellation kills the whole tree.** On Windows the runner spawns
150
+ `taskkill /PID <pid> /T /F`; on POSIX it starts the shell in its own process group and
151
+ sends `SIGTERM` to the group, then `SIGKILL` after 1.5 s. Neither path leaves an
152
+ orphaned child process behind.
153
+ - **Recognition covers both platforms.** The download/load classifier knows POSIX
154
+ transfer tools (`curl`, `wget`, `aria2c`, `rsync`, `scp`, `hf`, `pip`, …) and Windows
155
+ ones (`Invoke-WebRequest` / `iwr`, `Start-BitsTransfer`, `bitsadmin`, .NET
156
+ `DownloadFile`), plus both package-manager families: `apt`, `dnf`, `yum`, `zypper`,
157
+ `pacman`, `snap`, `flatpak`, `dpkg`, `rpm`, `brew` — and `apk`, `conda`, `pip`, npm and
158
+ the language package managers.
159
+ - **Verification.** Windows is exercised live on the development host; POSIX behaviour is
160
+ covered by unit tests that inject the platform and assert process-group
161
+ `SIGTERM`→`SIGKILL` versus `taskkill /T /F`, so no macOS/Linux host is required for
162
+ that coverage.
163
+
72
164
  ### Limits
73
165
 
74
- - **Foreground downloads are indeterminate.** A foreground tool call exposes no live
75
- output to the Host, so there is no percent/rate/ETA — only "a download is running".
166
+ - **Foreground rows are indeterminate, and appear only after 12 s.** A foreground tool
167
+ call exposes no live output to the Host, so there is no percent/rate/ETA — only "work is
168
+ running". The promoted row appears only after 12 s, by which time the call may already
169
+ be near its end.
76
170
  - **Session-scoped.** Rows belong to the session the strip is mounted in. A download
77
171
  started by a subagent/teammate session is not shown in the parent conversation.
78
- - **Opaque scripts need a hint.** See "What counts as a download" above.
172
+ - **Opaque scripts need a hint.** See "What counts as a load" above.
79
173
  - **No progress output, no meter.** `Invoke-WebRequest` with
80
174
  `$ProgressPreference = 'SilentlyContinue'` prints nothing to parse; the row still
81
175
  appears from the label hint but stays indeterminate.
82
- - **Stop only stops background jobs.** A foreground command cannot be killed from the
83
- HUD; it follows its own approval/timeout/cancellation path.
176
+ - **False positives cost one strip row.** Any job whose tail prints something meter-shaped
177
+ (`100%`, `[===]`, `eta`) is admitted; reads/searches are suppressed, but a plain script
178
+ that prints such text can still show up.
179
+ - **Stop only stops background jobs and `run_with_progress` rows.** A plain foreground
180
+ command cannot be killed from the HUD (the Host has no handle on it); use the tool when
181
+ a task must be cancellable.
182
+ - **The tool has no default timeout.** A hung command stays `running` until the user stops
183
+ it or the turn is cancelled (the tool call's own timeout still applies on top).
184
+ - **The model can bypass the tool.** The global prompt is guidance, not enforcement: a
185
+ shell call made directly falls back to the job/foreground classifier, and a plain
186
+ foreground command still shows no percent.
187
+ - **Rows linger.** A finished foreground row stays about 8 s and a `run_with_progress` row
188
+ 30 s, so a row can still be visible after its command has ended.
189
+ - **Host code reload needs a plugin reload.** The profile's HMR watch roots are empty
190
+ (`root: []`), so editing the Host half of a running app needs a bundle toggle or an app
191
+ restart; the Client half reloads with the usual client HMR.
84
192
  - UI rendering (strip/panel layout) can only be verified in a browser.
85
193
 
86
194
  ### Development
87
195
 
88
- Host and Client test harnesses live in `test/` in the source repository and are excluded
89
- from the published tarball:
90
-
91
- ```sh
92
- node plugins/dsh-transfer-hud/test/host.test.mjs
93
- node plugins/dsh-transfer-hud/test/client.test.mjs
94
- ```
196
+ The source repository ships Host, tool and Client test harnesses under `test/`; the
197
+ directory is excluded from the published tarball. From the source checkout, `npm test`
198
+ runs the Host harness under a mock context (classification, parsing, 12 s promotion,
199
+ session scoping, row cap), the real `run_with_progress` runner with short-lived Node child
200
+ processes (live row, kill, cancellation, 12 KB truncation), and the Client factory under
201
+ mock browser globals. No extra dependencies are needed.
95
202
 
96
203
  ### License
97
204
 
98
- MIT.
205
+ MIT. Copyright (c) 2026 xiuerhuahuazi.
99
206
 
100
207
  ## 中文
101
208
 
102
- 消息输入框下方的一条细长下载进度条,显示本 Harness 当前正在下载的内容,并可从进度条上
103
- 停止(后台)任务。零运行时依赖,由 Host 半(`index.js`)与 Web Client 半(`client.js`)组成。
209
+ 消息输入框下方的一条细长进度条,显示本 Harness 当前正在做的事——下载、安装、构建、模型
210
+ 加载以及任何长命令,并可从进度条上停止后台任务与 `run_with_progress` 命令。零运行时
211
+ 依赖,由 Host 半(`index.js`)与 Web Client 半(`client.js`)组成。
104
212
 
105
- > **版本**:本次 npm 发布为 **1.0.0**,打包的是工作区 HUD **v1.1.1**(细化后的下载分类器);
106
- > 首次公开发布将内部修订号重编为 1.0.0。
213
+ > **版本**:本次 npm 发布为 **1.1.0**,打包的是工作区任务进度 HUD **v1.3.0**(加载与
214
+ > 长任务、`run_with_progress` 工具、跨平台进程管理);首次公开发布曾将内部修订号重编为
215
+ > 1.0.0,1.1.0 对应工作区 v1.3.0。
107
216
 
108
217
  ### 显示内容
109
218
 
110
219
  | 来源 | 行 | 进度 |
111
220
  |---|---|---|
112
- | **后台任务**(`ctx.jobs`:用 `run_in_background` 启动、或超时转后台的 `pwsh`/`bash` 任务) | 真实任务行:标签、状态、**停止**按钮 | 输出环携带进度时显示百分比/字节/速度/剩余时间(curl、wget、aria2、tqdm / `hf download`、huggingface、modelscope、`git` 计数……),同时解析 **stdout 与 stderr**——多数下载器把进度打印到 stderr |
113
- | **前台 shell 命令**(`pwsh`、`bash`、`ops_bash`、`terminal_send`……) | 不确定行(`foreground: true`,无停止按钮) | 无——Host 只能看到调用,看不到其运行中的输出;该行用于表明正在下载,并在工具结果返回后不久消失 |
221
+ | **后台任务**(`ctx.jobs`:用 `run_in_background` 启动、或超时转后台的 `pwsh`/`bash` 任务) | 真实任务行:标签、状态、`下载` / `加载` 徽标、**停止**按钮 | 输出环携带进度时显示百分比/字节/速度/剩余时间(curl、wget、aria2、tqdm / `hf download`、huggingface、modelscope、`git` 计数、npm/pip/apt、ffmpeg、docker、`step 120/500`、`[=====> ]`……),同时解析 **stdout 与 stderr**——多数工具把进度打印到 stderr |
222
+ | **前台 shell 命令**(`pwsh`、`bash`、`ops_bash`、`terminal_send`……) | 当调用看起来像下载/加载、或已运行超过 12 秒时,显示不确定行(`foreground: true`,无停止按钮) | 无——Host 只能看到调用,看不到其运行中的输出;该行表明任务正在运行,并在工具结果返回约 8 秒后消失 |
223
+ | **`run_with_progress` 模型工具** | 真实行(`kind: 'tool'`,运行中带**停止**按钮) | **实时**百分比/字节/速度/剩余时间,来自命令合并后的 stdout+stderr(子进程由插件持有);命令结束后该行再保留 30 秒 |
114
224
 
115
225
  当前台调用启动后台任务(`run_in_background: true`,或超时转后台)时,任务行会替换前台行:
116
226
  Host 读取 `job/registered` 的 `toolCall.callId` 去重。
@@ -137,46 +247,127 @@ Host 读取 `job/registered` 的 `toolCall.callId` 去重。
137
247
  `python fetch.py` 这类不透明调用只有描述包含 download/fetch 等词、或文件/URL 命中已知
138
248
  产物模式时才会被识别。
139
249
 
250
+ ### 什么算作加载
251
+
252
+ 加载指除此之外一切明显在「干活」而不是在「回答」的任务。三类信号可让行出现:
253
+
254
+ - **任务输出里出现任何进度信号**——百分比、`done/total` 字节对、渲染条
255
+ (`[====> ]`)、tqdm 的迭代速率(`it/s`)、step / epoch / phase 计数。只要出现进度
256
+ 信号就算加载行,哪怕没有任何下载字样。
257
+ - **加载器命令**出现在任务标签或命令位置:`npm`/`pnpm`/`yarn` `install|ci|add`、
258
+ `apt(-get) install`、`dnf`/`yum`/`zypper install`、`pacman -S`、`snap`/`flatpak
259
+ install`、`dpkg -i`、`rpm -i`、`brew install`、`conda install`、`pip install`、
260
+ `docker pull|build`、`ffmpeg`、`cargo`/`go build`、`make`/`cmake`、
261
+ `dotnet build|restore`、`ollama pull`、macOS 的 `installer`/`pkgutil`,以及
262
+ 加载 / 构建 / 编译 / 安装 / 渲染 / 转码 / 导出 / 训练 / 解压 等动词。
263
+ - **没有任何线索的长前台调用**在运行满 12 秒后晋升为不确定的加载行。
264
+
265
+ 读取与搜索命令被排除:命令词是 `cat`/`Get-Content`/`grep`/`rg`/`find`/`ls`/`dir` 的
266
+ 任务或命令,不会因为输出里恰好出现 `100%` 或 `eta` 就变成加载。仅靠描述也不够,因此
267
+ 「Check build status」运行 `git status` 仍保持静默。后台任务的**标签**用更宽的历史线索
268
+ 扫描(整段多行脚本):后台任务更少见,漏掉一次加载的代价高于多出一条横条。
269
+
270
+ 只有下载专属证据——标签、tail 文本或前台分类器——才保留 `下载` 徽标;tqdm 训练任务属于
271
+ 加载,不是下载。
272
+
273
+ ### `run_with_progress` 工具
274
+
275
+ Host 半注册模型工具 `run_with_progress`,并注入一段短的全局系统提示,让模型在长操作时
276
+ 使用它。由于子进程由插件持有,经由该工具执行的命令能把实时百分比/速度/剩余时间流进横条,
277
+ 而普通前台 shell 调用对 Host 不暴露任何进度。
278
+
279
+ ```json
280
+ {
281
+ "command": "curl -# -L -o model.safetensors https://example.com/model.safetensors",
282
+ "label": "Downloading model weights"
283
+ }
284
+ ```
285
+
286
+ - **使用场景**:下载、安装、构建、模型加载、转码、数据集处理,以及任何预计超过约 10 秒
287
+ 的命令。工具描述与全局提示都要求这类任务优先走本工具,而不是普通的 `ops_bash` /
288
+ `pwsh` 调用。
289
+ - **参数**:`command`(必填字符串,一条 shell 命令行)与 `label`(可选,横条上的短标签)。
290
+ - **执行位置**:在调用会话的工作目录中执行,相对路径因此按会话工作区解析;`stdout` 与
291
+ `stderr` 合并后边到边解析,行与其他来源一样通过 `/dsh-download-hud/state` 提供。
292
+ - **并发**:最多同时运行 4 条(跨会话合计);第 5 次调用返回 `Busy` 文本结果且不建行。
293
+ - **取消**:用户可在行上点**停止**,Host 杀掉整棵进程树,行变为 `killed`,工具返回
294
+ `status: killed`;取消工具调用(回合中止)走同一条路径。
295
+ - **返回**:`status`、`exit code`、`duration` 与命令输出。输出 12 KB 以内完整返回;超出时
296
+ 保留开头 2 KB 与结尾 10 KB,并以 `[N characters truncated]` 标明截断。
297
+ - **保留进度输出**:提示要求 `curl -#` / `--progress-bar` 且绝不用 `-s`;`wget`/`pip`/
298
+ `npm`/`hf`/`aria2` 用默认进度输出;`ffmpeg` 加 `-progress pipe:1 -nostats`;PowerShell
299
+ 不要设 `$ProgressPreference = 'SilentlyContinue'`。
300
+ - **优雅降级**:缺少 `tools` / `systemPrompt` 服务的 Host 仍能加载插件、保留横条,只跳过
301
+ 工具注册或提示注入。
302
+
140
303
  ### 安装
141
304
 
142
305
  需要 DeepSeek Harness `>=0.2.1-rc.2 <0.3.0-0`,以及 Web 或 Desktop profile
143
306
  (插件同时包含 Host 半与浏览器 Client 半)。
144
307
 
145
- 1. 打开 **设置 → 插件**,安装 bundle `dsh-transfer-hud@1.0.0`。
308
+ 1. 打开 **设置 → 插件**,安装 bundle `dsh-transfer-hud@1.1.0`。
146
309
  2. 或让 agent 调用:`plugin_manager` → `action: "install_bundle"`,
147
- `target: "dsh-transfer-hud@1.0.0"`。
310
+ `target: "dsh-transfer-hud@1.1.0"`。
148
311
 
149
312
  **安装或升级后请重启应用。** HMR 会应用新的 bundle,但该 profile 的 Host watch root 为空
150
313
  (`root: []`),且替换已安装的包需要新的 JavaScript 模块代号,因此新的 Host 代码要重启后
151
314
  才加载;Client 半随后走常规的 client HMR / 页面刷新。验证方式:重启后运行一个前台下载
152
- (如 `curl -o file.bin <url>`)或后台任务(`run_in_background: true`),输入框下方会出现
153
- 进度条。移除该 bundle 即卸载。
315
+ (如 `curl -o file.bin <url>`)、后台任务(`run_in_background: true`),或经
316
+ `run_with_progress` 执行一条长命令,输入框下方会出现进度条。移除该 bundle 即卸载。
154
317
 
155
318
  ### 配置
156
319
 
157
320
  无。该行不需要配置,进度条自动出现,且只读取本 Harness 自身已有的活动。
158
321
 
322
+ ### 跨平台
323
+
324
+ 进度条与 `run_with_progress` 执行器本身与平台无关,不含原生代码——只使用 Node 的
325
+ `child_process` 与平台自带的进程控制命令。
326
+
327
+ - **Windows / macOS / Linux**:命令通过各平台默认 shell 执行(Windows 为 `ComSpec`,
328
+ POSIX 为 `/bin/sh`),普通可移植命令行无需改动。
329
+ - **取消会杀掉整棵进程树**:Windows 上调用 `taskkill /PID <pid> /T /F`;POSIX 上让 shell
330
+ 成为独立进程组组长,先向整组发 `SIGTERM`,1.5 秒后再发 `SIGKILL`。两条路径都不会留下
331
+ 孤儿进程。
332
+ - **命令识别覆盖两平台**:下载/加载分类器同时认识 POSIX 传输工具(`curl`、`wget`、
333
+ `aria2c`、`rsync`、`scp`、`hf`、`pip`……)与 Windows 工具(`Invoke-WebRequest` /
334
+ `iwr`、`Start-BitsTransfer`、`bitsadmin`、.NET `DownloadFile`),以及两套包管理器:
335
+ `apt`、`dnf`、`yum`、`zypper`、`pacman`、`snap`、`flatpak`、`dpkg`、`rpm`、`brew`,
336
+ 另有 `apk`、`conda`、`pip`、npm 及各语言包管理器。
337
+ - **验证方式**:Windows 在开发机上做 live 验证;POSIX 行为由注入平台的单测覆盖,断言
338
+ 进程组 `SIGTERM`→`SIGKILL` 与 `taskkill /T /F` 两条分支——因此不需要在 CI 中准备
339
+ macOS/Linux 真机。
340
+
159
341
  ### 限制
160
342
 
161
- - **前台下载是不确定的**:前台工具调用不向 Host 暴露运行中输出,所以没有百分比/速度/
162
- 剩余时间,只表示“正在下载”。
343
+ - **前台行是不确定的,且要 12 秒后才出现**:前台工具调用不向 Host 暴露运行中输出,所以
344
+ 没有百分比/速度/剩余时间,只表示「正在干活」;晋升行要等 12 秒,此时调用可能已接近结束。
163
345
  - **按会话隔离**:行属于进度条挂载的会话;子代理/队友会话发起的下载不会显示在父会话中。
164
- - **不透明脚本需要提示**:见上文“什么算作下载”。
346
+ - **不透明脚本需要提示**:见上文「什么算作加载」。
165
347
  - **没有进度输出就没有进度**:`Invoke-WebRequest` 在
166
348
  `$ProgressPreference = 'SilentlyContinue'` 下不打印可解析内容;行仍会按标签提示出现,
167
349
  但保持不确定状态。
168
- - **停止只作用于后台任务**:前台命令无法从进度条取消,它遵循自身的审批/超时/取消机制。
350
+ - **误报的代价只有一条横条**:任务 tail 里出现进度形状(`100%`、`[===]`、`eta`)的任务都
351
+ 会被接纳;读取/搜索已抑制,但普通脚本打印这类文本时仍可能出行。
352
+ - **停止只作用于后台任务与 `run_with_progress` 行**:普通前台命令无法从进度条取消
353
+ (Host 没有它的句柄);需要可取消时请使用该工具。
354
+ - **该工具没有默认超时**:挂死的命令会一直保持 `running`,直到用户停止或回合被取消
355
+ (工具调用自身的超时仍叠加生效)。
356
+ - **模型可能绕开工具**:全局提示只是引导而非强制;直接发起的 shell 调用回落到任务/前台
357
+ 分类器,普通前台命令依旧没有百分比。
358
+ - **行会短暂保留**:结束后的前台行约保留 8 秒、`run_with_progress` 行保留 30 秒,因此
359
+ 命令结束后行可能还短暂可见。
360
+ - **Host 代码重载需要重载插件**:该 profile 的 HMR watch root 为空(`root: []`),修改
361
+ 运行中应用的 Host 半需要切换 bundle 或重启应用;Client 半走常规 client HMR。
169
362
  - 界面渲染(进度条/浮层布局)只能在浏览器中验证。
170
363
 
171
364
  ### 开发
172
365
 
173
- Host 与 Client 的测试位于源码仓库的 `test/`,不包含在发布的 tarball 中:
174
-
175
- ```sh
176
- node plugins/dsh-transfer-hud/test/host.test.mjs
177
- node plugins/dsh-transfer-hud/test/client.test.mjs
178
- ```
366
+ 源码仓库的 `test/` 下包含 Host、工具与 Client 三套测试,该目录不包含在发布的 tarball 中。
367
+ 在源码检出中运行 `npm test`:Host 套件在 mock 上下文下覆盖分类、解析、12 秒晋升、会话隔离
368
+ 与行数上限,真实 `run_with_progress` 执行器用短命的 Node 子进程覆盖实时行、停止、取消与
369
+ 12 KB 截断,Client 工厂在 mock 浏览器全局对象下运行。无需额外依赖。
179
370
 
180
371
  ### 许可证
181
372
 
182
- MIT。
373
+ MIT。Copyright (c) 2026 xiuerhuahuazi。
package/client.js CHANGED
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Download HUD — Client half.
3
3
  *
4
- * A thin, space-free strip below the message composer that appears only while a
5
- * background job looks like a download. Clicking it opens a details popover with
6
- * per-job progress, throughput, remaining time and a stop action.
4
+ * A thin, space-free strip below the message composer that appears while any
5
+ * background job reports progress: downloads with a meter (percent, throughput,
6
+ * remaining time) and long-running loads/jobs without one (running + elapsed).
7
+ * Clicking it opens a details popover with per-job progress and a stop action.
8
+ *
9
+ * Rows carry `category: 'download' | 'load'`. Rows from an older Host that only
10
+ * carry `download === true` are still rendered, as downloads. Rows with neither
11
+ * are ignored, so unrelated background jobs never leak into the HUD.
7
12
  *
8
13
  * Data comes from the Host half's same-origin route; this module renders only.
9
14
  */
@@ -23,8 +28,8 @@ window.__ModuleLoader__.load({
23
28
 
24
29
  const TEXT = {
25
30
  en: {
26
- live: 'Downloading',
27
- jobs: 'downloads',
31
+ live: 'Running',
32
+ jobs: 'tasks',
28
33
  stopped: 'Stopped',
29
34
  done: 'Done',
30
35
  failed: 'Failed',
@@ -32,13 +37,16 @@ window.__ModuleLoader__.load({
32
37
  stop: 'Stop',
33
38
  stopping: 'Stopping…',
34
39
  noProgress: 'running',
35
- details: 'Downloads',
40
+ details: 'Task progress',
36
41
  elapsed: 'elapsed',
37
42
  command: 'command',
43
+ aria: 'Running tasks',
44
+ kindDownload: 'download',
45
+ kindLoad: 'load',
38
46
  },
39
47
  zh: {
40
- live: '下载中',
41
- jobs: '个下载',
48
+ live: '进行中',
49
+ jobs: '个任务',
42
50
  stopped: '已停止',
43
51
  done: '已完成',
44
52
  failed: '失败',
@@ -46,9 +54,12 @@ window.__ModuleLoader__.load({
46
54
  stop: '取消',
47
55
  stopping: '正在停止…',
48
56
  noProgress: '运行中',
49
- details: '下载任务',
57
+ details: '任务进度',
50
58
  elapsed: '已用时',
51
59
  command: '命令',
60
+ aria: '进行中的任务',
61
+ kindDownload: '下载',
62
+ kindLoad: '加载',
52
63
  },
53
64
  }
54
65
 
@@ -84,6 +95,9 @@ window.__ModuleLoader__.load({
84
95
  .ddh-stop:disabled{opacity:.5;cursor:default}
85
96
  .ddh-row-sub{display:flex;gap:10px;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
86
97
  .ddh-row-bar{height:2px;border-radius:1px;background:var(--dsw-alias-border-l1);overflow:hidden;margin-top:2px}
98
+ .ddh-kind{flex:none;font-size:10px;line-height:14px;padding:0 4px;border-radius:4px;border:0.5px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);white-space:nowrap}
99
+ .ddh-kind-download{color:var(--dsw-alias-label-secondary)}
100
+ .ddh-kind-load{background:var(--dsw-alias-bg-layer-2);border-color:transparent;color:var(--dsw-alias-brand-primary)}
87
101
  .ddh-note{padding:8px 12px;color:var(--dsw-alias-state-error-primary);font-size:11px}
88
102
  `
89
103
 
@@ -130,6 +144,16 @@ window.__ModuleLoader__.load({
130
144
  return job.status === 'running' || job.status === 'stopping'
131
145
  }
132
146
 
147
+ // The Host labels each row `category: 'download' | 'load'`. Older Host
148
+ // halves only sent `download: true`; treat those as downloads for
149
+ // compatibility. Anything else is not a progress task and stays hidden.
150
+ function categoryOf(job) {
151
+ if (!job || typeof job !== 'object') return ''
152
+ if (job.category === 'download' || job.category === 'load') return job.category
153
+ if (job.download === true) return 'download'
154
+ return ''
155
+ }
156
+
133
157
  function statusText(job, text) {
134
158
  if (job.status === 'completed') return text.done
135
159
  if (job.status === 'failed') return text.failed
@@ -141,7 +165,7 @@ window.__ModuleLoader__.load({
141
165
  function visibleJobs(state, now) {
142
166
  const jobs = state && Array.isArray(state.jobs) ? state.jobs : []
143
167
  return jobs.filter((job) => {
144
- if (!job || job.download !== true) return false
168
+ if (!categoryOf(job)) return false
145
169
  if (isLive(job)) return true
146
170
  const finishedAt = typeof job.finishedAt === 'number' ? job.finishedAt : 0
147
171
  return finishedAt > 0 && now - finishedAt < LINGER_MS
@@ -319,6 +343,7 @@ window.__ModuleLoader__.load({
319
343
  h('span', { key: 'count' }, `${jobs.length}`),
320
344
  ]),
321
345
  h('div', { key: 'rows', className: 'ddh-rows' }, jobs.map((job) => {
346
+ const category = categoryOf(job)
322
347
  const dotSuffix = job.status === 'completed' ? ' ddh-dot-done'
323
348
  : job.status === 'failed' ? ' ddh-dot-failed'
324
349
  : isLive(job) ? '' : ' ddh-dot-idle'
@@ -341,6 +366,10 @@ window.__ModuleLoader__.load({
341
366
  h('div', { key: 'top', className: 'ddh-row-top' }, [
342
367
  h('span', { key: 'dot', className: `ddh-dot${dotSuffix}` }),
343
368
  h('span', { key: 'name', className: 'ddh-row-name', title: job.fullLabel || job.label }, job.label),
369
+ h('span', {
370
+ key: 'kind',
371
+ className: `ddh-kind ddh-kind-${category}`,
372
+ }, category === 'load' ? text.kindLoad : text.kindDownload),
344
373
  rowPercent ? h('span', { key: 'pct', className: 'ddh-row-pct' }, rowPercent) : null,
345
374
  isLive(job) && job.killable !== false
346
375
  ? h('button', {
@@ -379,6 +408,7 @@ window.__ModuleLoader__.load({
379
408
  type: 'button',
380
409
  className: 'ddh-strip',
381
410
  'aria-expanded': open ? 'true' : 'false',
411
+ 'aria-label': text.aria,
382
412
  onClick: () => setOpen((previous) => !previous),
383
413
  }, [
384
414
  h('span', { key: 'line', className: 'ddh-line' }, [
@@ -421,6 +451,8 @@ window.__ModuleLoader__.load({
421
451
 
422
452
  return {
423
453
  inject: ['slots', 'locale'],
454
+ // White-box seam for test/client.test.mjs only; the loader ignores it.
455
+ __test: { TEXT, categoryOf, visibleJobs },
424
456
  apply(ctx) {
425
457
  const text = TEXT[pickLocale(ctx)]
426
458
  const Component = createDownloadHud(ctx, text)