dsh-job-progress 0.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/lib/client.js ADDED
@@ -0,0 +1,490 @@
1
+ /**
2
+ * dsh-job-progress — client half.
3
+ *
4
+ * A floating ball in the conversation window. It appears only when this session
5
+ * has something to watch, carries the live task count, can be dragged anywhere
6
+ * (the position is remembered), and expands into the task panel on click.
7
+ * The panel can clear what has already finished.
8
+ *
9
+ * Hand-written against the module-loader contract (no bundler step): the shell
10
+ * evaluates this file, the factory receives `require`, and the exported `apply`
11
+ * runs with the client root context.
12
+ */
13
+ window.__ModuleLoader__.load({
14
+ id: 'dsh-job-progress',
15
+ factory: (require) => {
16
+ // 模块加载器契约:factory 收到 require,必须返回 module.exports。下面两行不能省 ——
17
+ // 2026-09-20 就是漏了它们:客户端半边一加载就抛 ReferenceError: exports is not defined,
18
+ // 渲染层的插件图随之崩掉、界面起不来,而宿主半边照常挂载、日志里一条异常都没有。
19
+ const module = { exports: {} };
20
+ const exports = module.exports;
21
+ const React = require('react');
22
+ const h = React.createElement;
23
+ const NS = 'jobProgress';
24
+ const POLL_MS = 2000;
25
+ const POLL_IDLE_MS = 10000;
26
+ const EMPTY = [];
27
+ const BALL = 56;
28
+ const PANEL_W = 340;
29
+ const PANEL_MAX_H = 420;
30
+ const POS_KEY = 'dsh-job-progress:pos';
31
+ const DISMISS_PREFIX = 'dsh-job-progress:dismissed:';
32
+
33
+ /** 球里的图标:鲸鱼娘「吃白饭的底气」表情包的方形版,缩到 144px 后内联。 * 内联而不是走宿主路由 —— 客户端半边是每次刷新页面重载的,换图不用重启 DSH; * 代价是 client.js 多了几十 KB(要换成大图/动图再改走宿主路由)。 */ const ICON_DATA = 'data:image/jpeg;base64,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';
34
+
35
+ /** The connection service, bound in `apply` (module scope keeps the row simple). */
36
+ let connection = null;
37
+
38
+ // 存储三件套:预检(Node 里没有 localStorage)与隐私模式都必须安静降级。
39
+ function readStore(key) {
40
+ try {
41
+ return window.localStorage.getItem(key);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function writeStore(key, value) {
47
+ try {
48
+ window.localStorage.setItem(key, value);
49
+ } catch {
50
+ /* 存不下就不存:位置与已清除记录都只是便利,不是正确性 */
51
+ }
52
+ }
53
+ function readJson(key, fallback) {
54
+ const raw = readStore(key);
55
+ if (raw === null) return fallback;
56
+ try {
57
+ const value = JSON.parse(raw);
58
+ return value ?? fallback;
59
+ } catch {
60
+ return fallback;
61
+ }
62
+ }
63
+ function viewport() {
64
+ const w = typeof window !== 'undefined' && Number.isFinite(window.innerWidth) ? window.innerWidth : 1280;
65
+ const h = typeof window !== 'undefined' && Number.isFinite(window.innerHeight) ? window.innerHeight : 800;
66
+ return { w, h };
67
+ }
68
+
69
+ const zh = {
70
+ 'count.live': '{count} 个任务进行中',
71
+ 'count.total': '{count} 个任务',
72
+ 'list.aria': '本会话的任务进度',
73
+ 'ball.title': '本会话的长任务与进度(可拖动)',
74
+ 'status.running': '进行中',
75
+ 'status.lost': '已失联',
76
+ 'status.done': '完成',
77
+ 'status.failed': '失败',
78
+ 'status.completed': '完成',
79
+ 'status.killed': '已取消',
80
+ 'status.stopping': '停止中',
81
+ 'phase.merging': '合并中',
82
+ 'phase.verifying': '校验中',
83
+ 'empty': '本会话暂无长任务',
84
+ 'eta.left': '剩{d}',
85
+ 'clear.button': '清除已完成',
86
+ 'clear.hint': '{count} 项已完成',
87
+ 'clear.none': '没有已完成的任务',
88
+ 'clear.done': '已清除 {count} 项',
89
+ 'clear.failed': '清除失败:宿主未就绪'
90
+ };
91
+ const en = {
92
+ 'count.live': '{count} running',
93
+ 'count.total': '{count} task(s)',
94
+ 'list.aria': 'Task progress for this session',
95
+ 'ball.title': 'Long-running tasks and their progress (draggable)',
96
+ 'status.running': 'running',
97
+ 'status.lost': 'lost',
98
+ 'status.done': 'done',
99
+ 'status.failed': 'failed',
100
+ 'status.completed': 'done',
101
+ 'status.killed': 'cancelled',
102
+ 'status.stopping': 'stopping',
103
+ 'phase.merging': 'merging',
104
+ 'phase.verifying': 'verifying',
105
+ 'empty': 'No long-running tasks in this session',
106
+ 'eta.left': '{d} left',
107
+ 'clear.button': 'Clear finished',
108
+ 'clear.hint': '{count} finished',
109
+ 'clear.none': 'Nothing finished yet',
110
+ 'clear.done': 'Cleared {count}',
111
+ 'clear.failed': 'Clear failed: host not ready'
112
+ };
113
+ const fallbackT = (key, params) => {
114
+ const template = zh[key] ?? key;
115
+ if (params === undefined) return template;
116
+ return template.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? ''));
117
+ };
118
+
119
+ const CSS = [
120
+ '.djp-root{position:fixed;inset:0;pointer-events:none;z-index:9000}',
121
+ '.djp-root>*{pointer-events:auto}',
122
+ '.djp-ball{position:fixed;box-sizing:border-box;width:56px;height:56px;display:flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:50%;cursor:grab;touch-action:none;color:#fff;background:linear-gradient(150deg,#3d8bf0,#1b4fa0);box-shadow:0 0 0 2px rgba(255,255,255,.92),0 0 0 3px rgba(18,30,58,.28),0 6px 18px rgba(18,38,84,.34)}',
123
+ '.djp-ball:hover{filter:brightness(1.07)}',
124
+ '.djp-ball:active{cursor:grabbing}',
125
+ '.djp-ball:focus-visible{outline:2px solid #7fb0ff;outline-offset:2px}',
126
+ '.djp-icon{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%;pointer-events:none;-webkit-user-drag:none}',
127
+ '.djp-badge{position:absolute;top:-5px;right:-5px;box-sizing:border-box;width:19px;height:19px;border-radius:50%;background:#395590;color:#fff;font-size:10.5px;line-height:19px;text-align:center;font-variant-numeric:tabular-nums;box-shadow:0 0 0 2px rgba(255,255,255,.9)}',
128
+ '.djp-panel{position:fixed;z-index:9001;box-sizing:border-box;width:340px;display:flex;flex-direction:column;gap:6px;overflow:auto;padding:8px;border-radius:14px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-elevation-prominent)}',
129
+ '.djp-row{display:flex;flex-direction:column;gap:5px;box-sizing:border-box;width:100%;padding:7px 8px;border-radius:8px;color:var(--dsw-alias-label-primary);font-size:12.5px;line-height:17px}',
130
+ '.djp-rowSettled{color:var(--dsw-alias-label-tertiary)}',
131
+ '.djp-head{display:flex;align-items:center;gap:7px}',
132
+ '.djp-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}',
133
+ '.djp-dot-running{background:#2f7de1}',
134
+ '.djp-dot-done{background:#2f9e6b}',
135
+ '.djp-dot-failed{background:#c9433a}',
136
+ '.djp-dot-lost{background:#a8a8a8}',
137
+ '.djp-label{flex:1;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-family:var(--dsw-font-mono)}',
138
+ '.djp-status{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
139
+ '.djp-bar{position:relative;height:4px;overflow:hidden;border-radius:2px;background:var(--dsw-alias-fill-l2)}',
140
+ '.djp-barFill{position:absolute;top:0;left:0;height:100%;border-radius:2px;background:#2f7de1}',
141
+ '.djp-barFill-done{background:#2f9e6b}',
142
+ '.djp-barFill-failed{background:#c9433a}',
143
+ '.djp-meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
144
+ '.djp-note{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}',
145
+ '.djp-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px 8px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}',
146
+ '.djp-hint{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
147
+ '.djp-clear{flex:none;padding:5px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;font-size:12px}',
148
+ '.djp-clear:hover:enabled{background:var(--dsw-alias-fill-l2)}',
149
+ '.djp-clear:disabled{opacity:.45;cursor:default}'
150
+ ].join('');
151
+
152
+ function injectStyles() {
153
+ if (typeof document === 'undefined') return;
154
+ const tagId = 'dsh-job-progress/styles';
155
+ if (document.querySelector(`style[data-plugin-css="${tagId}"]`) !== null) return;
156
+ const tag = document.createElement('style');
157
+ tag.dataset.plugin = 'dsh-job-progress';
158
+ tag.dataset.pluginCss = tagId;
159
+ tag.textContent = CSS;
160
+ document.head.appendChild(tag);
161
+ }
162
+
163
+ function formatBytes(value) {
164
+ const n = Number(value);
165
+ if (!Number.isFinite(n) || n <= 0) return '0B';
166
+ if (n >= 1073741824) return `${(n / 1073741824).toFixed(2)}GB`;
167
+ if (n >= 1048576) return `${(n / 1048576).toFixed(1)}MB`;
168
+ if (n >= 1024) return `${(n / 1024).toFixed(0)}KB`;
169
+ return `${Math.round(n)}B`;
170
+ }
171
+
172
+ function formatDuration(ms) {
173
+ const total = Math.max(0, Math.floor(Number(ms) / 1000));
174
+ const seconds = total % 60;
175
+ const minutes = Math.floor(total / 60) % 60;
176
+ const hours = Math.floor(total / 3600);
177
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, '0')}m`;
178
+ if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, '0')}s`;
179
+ return `${seconds}s`;
180
+ }
181
+
182
+ function percent(task) {
183
+ if (Number(task.total) > 0) return Math.max(0, Math.min(100, Math.round((Number(task.done) / Number(task.total)) * 100)));
184
+ if (task.status === 'done' || task.status === 'completed') return 100;
185
+ return null;
186
+ }
187
+
188
+ function isLive(task) {
189
+ return task.status === 'running' || task.status === 'stopping';
190
+ }
191
+
192
+ /** 显示层兜底去重:宿主已按作业 id 去重,这里防的是多来源重叠(旧宿主 / 多个进度目录)。 */
193
+ function dedupe(tasks) {
194
+ const seen = new Set();
195
+ const out = [];
196
+ for (const task of tasks) {
197
+ const key = String(task.key ?? task.jobId ?? task.label ?? '');
198
+ if (key.length > 0) {
199
+ if (seen.has(key)) continue;
200
+ seen.add(key);
201
+ }
202
+ out.push(task);
203
+ }
204
+ return out.length === tasks.length ? tasks : out;
205
+ }
206
+
207
+ /**
208
+ * 忽略名单的键。作业 id 是 <kind>-N、按进程从 1 计数,重启后新作业会又叫 pwsh-1;
209
+ * 所以键里必须带上 startedAt,否则上一轮的忽略记录会把新作业误挡在面板外
210
+ * (2026-09-20 实测:重启后新起的 pwsh-1 被旧记录挡住)。
211
+ */
212
+ function dismissKey(task) {
213
+ return String(task.jobId ?? task.key ?? task.label ?? '') + '@' + String(task.startedAt ?? 0);
214
+ }
215
+
216
+ function statusText(task, t) {
217
+ if (task.status === 'running' && task.phase === 'merging') return t('phase.merging');
218
+ if (task.status === 'running' && task.phase === 'verifying') return t('phase.verifying');
219
+ if (task.status === 'running') return t('status.running');
220
+ if (task.status === 'lost') return t('status.lost');
221
+ const known = t(`status.${task.status}`);
222
+ return known === `status.${task.status}` ? String(task.status ?? '') : known;
223
+ }
224
+
225
+ /** Ask the host for this session's merged task list. */
226
+ async function queryProgress(sessionId) {
227
+ if (connection === null || sessionId === undefined) return null;
228
+ try {
229
+ const result = await connection.rpc.call('/api', 'jobProgress/snapshot', {
230
+ args: { request: { sessionId } }
231
+ });
232
+ if (result !== null && typeof result === 'object' && result.ok === true) return result.value;
233
+ return null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ /** Delete the finished progress files of this session (running ones are never touched). */
240
+ async function clearProgress(sessionId) {
241
+ if (connection === null || sessionId === undefined) return null;
242
+ try {
243
+ const result = await connection.rpc.call('/api', 'jobProgress/clear', {
244
+ args: { request: { sessionId } }
245
+ });
246
+ if (result !== null && typeof result === 'object' && result.ok === true) return result.value;
247
+ return null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /** 球的位置:记住上次拖到哪,但每次都要按当前视口夹紧(窗口可能变小了)。 */
254
+ function loadPosition() {
255
+ const { w, h } = viewport();
256
+ const saved = readJson(POS_KEY, null);
257
+ const fallback = { x: w - BALL - 24, y: Math.max(8, Math.round(h * 0.62)) };
258
+ if (saved === null || !Number.isFinite(saved.x) || !Number.isFinite(saved.y)) return fallback;
259
+ return {
260
+ x: Math.min(Math.max(8, saved.x), Math.max(8, w - BALL - 8)),
261
+ y: Math.min(Math.max(8, saved.y), Math.max(8, h - BALL - 8))
262
+ };
263
+ }
264
+
265
+ /** One row: label, status, progress bar, numbers, elapsed time. */
266
+ function Row(props) {
267
+ const task = props.task;
268
+ const t = props.t;
269
+ const now = props.now;
270
+ const live = isLive(task);
271
+ const pct = percent(task);
272
+ const elapsedMs = live ? now - (task.startedAt ?? now) : (task.finishedAt ?? task.updatedAt ?? now) - (task.startedAt ?? now);
273
+ const numbers = [];
274
+ if (task.hasProgress && pct !== null) numbers.push(`${pct}%`);
275
+ if (task.hasProgress && Number(task.total) > 0) {
276
+ numbers.push(task.unit === 'count' ? `${task.done}/${task.total}` : `${formatBytes(task.done)}/${formatBytes(task.total)}`);
277
+ }
278
+ if (live && Number(task.speed) > 0) numbers.push(task.unit === 'count' ? `${Number(task.speed).toFixed(1)}/s` : `${formatBytes(task.speed)}/s`);
279
+ if (live && Number(task.eta) > 0) numbers.push(t('eta.left', { d: formatDuration(Number(task.eta) * 1000) }));
280
+ numbers.push(formatDuration(elapsedMs));
281
+ return h('li', { className: live ? 'djp-row' : 'djp-row djp-rowSettled' },
282
+ h('div', { className: 'djp-head' },
283
+ h('span', { className: `djp-dot djp-dot-${task.status}` }),
284
+ h('span', { className: 'djp-label', title: task.label }, String(task.label ?? '')),
285
+ h('span', { className: 'djp-status' }, statusText(task, t))
286
+ ),
287
+ task.hasProgress && pct !== null
288
+ ? h('div', { className: 'djp-bar' },
289
+ h('div', {
290
+ className: task.status === 'done' || task.status === 'completed' ? 'djp-barFill djp-barFill-done' : task.status === 'failed' ? 'djp-barFill djp-barFill-failed' : 'djp-barFill',
291
+ style: { width: `${pct}%` }
292
+ }))
293
+ : null,
294
+ h('div', { className: 'djp-meta' }, numbers.join(' · ')),
295
+ task.note ? h('div', { className: 'djp-note', title: task.note }, task.note) : null
296
+ );
297
+ }
298
+
299
+ function JobProgressAction(props) {
300
+ const sessionId = props.sessionId;
301
+ const useSessions = props.useSessions;
302
+ const t = props.t ?? fallbackT;
303
+ const jobs = useSessions((state) => (state.jobsBySession ?? {})[sessionId]) ?? EMPTY;
304
+ const [tasks, setTasks] = React.useState(EMPTY);
305
+ const [open, setOpen] = React.useState(false);
306
+ const [now, setNow] = React.useState(() => Date.now());
307
+ const [pos, setPos] = React.useState(() => loadPosition());
308
+ const [flash, setFlash] = React.useState('');
309
+ const [dismissed, setDismissed] = React.useState(() => new Set(readJson(DISMISS_PREFIX + sessionId, [])));
310
+ const rootRef = React.useRef(null);
311
+ const dragRef = React.useRef(null);
312
+ const jobCount = jobs.length;
313
+
314
+ React.useEffect(() => {
315
+ let alive = true;
316
+ let timer = null;
317
+ const pull = async () => {
318
+ const value = await queryProgress(sessionId);
319
+ if (!alive || value === null) return;
320
+ setTasks(dedupe(Array.isArray(value.tasks) ? value.tasks : EMPTY));
321
+ };
322
+ pull();
323
+ timer = setInterval(pull, jobCount > 0 ? POLL_MS : POLL_IDLE_MS);
324
+ return () => {
325
+ alive = false;
326
+ if (timer !== null) clearInterval(timer);
327
+ };
328
+ }, [sessionId, jobCount]);
329
+
330
+ React.useEffect(() => {
331
+ if (!open) return undefined;
332
+ setNow(Date.now());
333
+ const timer = setInterval(() => setNow(Date.now()), 1000);
334
+ return () => clearInterval(timer);
335
+ }, [open]);
336
+
337
+ // flash 只活 4 秒,之后回到「N 项已完成」的常规提示 React.useEffect(() => { if (flash === '') return undefined; const timer = setTimeout(() => setFlash(''), 4000); return () => clearTimeout(timer); }, [flash]);
338
+
339
+ React.useEffect(() => {
340
+ if (!open) return undefined;
341
+ const onPointer = (event) => {
342
+ if (rootRef.current !== null && !rootRef.current.contains(event.target)) setOpen(false);
343
+ };
344
+ const onKey = (event) => {
345
+ if (event.key === 'Escape') setOpen(false);
346
+ };
347
+ document.addEventListener('pointerdown', onPointer, true);
348
+ document.addEventListener('keydown', onKey, true);
349
+ return () => {
350
+ document.removeEventListener('pointerdown', onPointer, true);
351
+ document.removeEventListener('keydown', onKey, true);
352
+ };
353
+ }, [open]);
354
+
355
+ // 终态作业在注册表里删不掉(只读投影),所以「清除」把它们记进本地忽略名单。
356
+ const visible = React.useMemo(() => dedupe(tasks).filter((task) => {
357
+ if (isLive(task)) return true;
358
+ const id = dismissKey(task);
359
+ return !dismissed.has(id);
360
+ }), [tasks, dismissed]);
361
+
362
+ const onPointerDown = (event) => {
363
+ if (typeof event.button === 'number' && event.button !== 0) return;
364
+ dragRef.current = { id: event.pointerId, sx: event.clientX, sy: event.clientY, ox: pos.x, oy: pos.y, moved: false };
365
+ try {
366
+ event.currentTarget.setPointerCapture(event.pointerId);
367
+ } catch {
368
+ /* 捕获失败不影响拖动本身 */
369
+ }
370
+ };
371
+ const onPointerMove = (event) => {
372
+ const drag = dragRef.current;
373
+ if (drag === null || drag.id !== event.pointerId) return;
374
+ const dx = event.clientX - drag.sx;
375
+ const dy = event.clientY - drag.sy;
376
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 5) return;
377
+ drag.moved = true;
378
+ const { w, h } = viewport();
379
+ const next = {
380
+ x: Math.min(Math.max(8, drag.ox + dx), Math.max(8, w - BALL - 8)),
381
+ y: Math.min(Math.max(8, drag.oy + dy), Math.max(8, h - BALL - 8))
382
+ };
383
+ setPos(next);
384
+ writeStore(POS_KEY, JSON.stringify(next));
385
+ };
386
+ const onPointerUp = (event) => {
387
+ const drag = dragRef.current;
388
+ dragRef.current = null;
389
+ if (drag === null || drag.moved) return; // 拖动过就不算点击
390
+ setOpen((current) => !current);
391
+ };
392
+
393
+ const clearFinished = async () => {
394
+ const finished = visible.filter((task) => !isLive(task));
395
+ const result = await clearProgress(sessionId);
396
+ if (result === null) {
397
+ // 宿主没应答(旧宿主 / 未重启 / RPC 不可达)时绝不能报成功:2026-09-20 实测踩到 ——
398
+ // 宿主还是旧版、没有 clear 端点(404),界面照样报「已清除 3 项」并把行藏了,
399
+ // 而磁盘上一个文件都没少。看不见的谎报比看得见的失败更坏。
400
+ setFlash(t('clear.failed'));
401
+ return;
402
+ }
403
+ const next = new Set(dismissed);
404
+ for (const task of finished) next.add(dismissKey(task));
405
+ setDismissed(next);
406
+ writeStore(DISMISS_PREFIX + sessionId, JSON.stringify([...next].slice(-200)));
407
+ const value = await queryProgress(sessionId);
408
+ if (value !== null) setTasks(dedupe(Array.isArray(value.tasks) ? value.tasks : EMPTY));
409
+ // 清除成功后不报数(按用户要求):hint 会自然回到「没有已完成的任务」。
410
+ };
411
+
412
+ // 判据一:有可见任务就亮相(进度数字随后到)。
413
+ // 判据二:镜像里还有「没被清除掉的」作业 —— 用作业 id + startedAt 去比忽略名单,
414
+ // 这样新起的作业立刻就可见,而清除之后球会真的消失(2026-09-20 实测两处都踩过:
415
+ // 只按 jobCount 判会让清除后球赖着不走;只按 visible 判则 RPC 慢时球根本不出现)。
416
+ const hasUndismissedJob = jobs.some((job) => !dismissed.has(dismissKey({ jobId: job.id, startedAt: job.startedAt })));
417
+ if (visible.length === 0 && !hasUndismissedJob) return null;
418
+
419
+ const liveCount = visible.filter(isLive).length;
420
+ const finishedCount = visible.length - liveCount;
421
+ const { w: vw, h: vh } = viewport();
422
+ const panelLeft = Math.min(Math.max(8, pos.x + BALL - PANEL_W), Math.max(8, vw - PANEL_W - 8));
423
+ const openUpward = pos.y > vh * 0.45;
424
+ const panelStyle = openUpward
425
+ ? { left: panelLeft, bottom: Math.max(8, vh - pos.y + 12), maxHeight: Math.min(PANEL_MAX_H, Math.max(160, pos.y - 24)) }
426
+ : { left: panelLeft, top: Math.min(vh - 160, pos.y + BALL + 12), maxHeight: Math.min(PANEL_MAX_H, Math.max(160, vh - pos.y - BALL - 24)) };
427
+
428
+ return h('div', { className: 'djp-root', ref: rootRef },
429
+ h('button', {
430
+ type: 'button',
431
+ className: 'djp-ball',
432
+ style: { left: pos.x, top: pos.y },
433
+ title: t('ball.title'),
434
+ 'aria-label': t('list.aria'),
435
+ 'aria-expanded': open,
436
+ onPointerDown,
437
+ onPointerMove,
438
+ onPointerUp,
439
+ onKeyDown: (event) => {
440
+ if (event.key === 'Enter' || event.key === ' ') {
441
+ event.preventDefault();
442
+ setOpen((current) => !current);
443
+ }
444
+ }
445
+ },
446
+ h('img', { className: 'djp-icon', src: ICON_DATA, alt: '', draggable: false }),
447
+ liveCount > 0 ? h('span', { className: 'djp-badge', title: t('count.live', { count: liveCount }) }, liveCount > 9 ? '9+' : String(liveCount)) : null
448
+ ),
449
+ open
450
+ ? h('div', { className: 'djp-panel', style: panelStyle, role: 'dialog', 'aria-label': t('list.aria') },
451
+ h('div', { className: 'djp-foot' },
452
+ h('span', { className: 'djp-hint', title: flash || t('clear.hint', { count: finishedCount }) },
453
+ flash !== '' ? flash : finishedCount > 0 ? t('clear.hint', { count: finishedCount }) : t('clear.none')),
454
+ h('button', {
455
+ type: 'button',
456
+ className: 'djp-clear',
457
+ disabled: finishedCount === 0,
458
+ onClick: clearFinished
459
+ }, t('clear.button'))
460
+ ),
461
+ h('ul', { className: 'djp-list', style: { display: 'flex', flexDirection: 'column', gap: 4, margin: 0, padding: 0, listStyle: 'none' } },
462
+ visible.length === 0
463
+ ? h('li', { className: 'djp-row djp-rowSettled' }, t('empty'))
464
+ : visible.map((task) => h(Row, { key: task.key ?? task.label, task, t, now })))
465
+ )
466
+ : null
467
+ );
468
+ }
469
+
470
+ /** Required client services: session store, slot registry, locale, RPC carrier. */
471
+ const inject = ['sessions', 'slots', 'locale', 'connection'];
472
+
473
+ /** Client plugin body: dictionaries, styles, then the floating ball. */
474
+ function apply(ctx) {
475
+ connection = ctx.connection ?? null;
476
+ ctx.effect(() => injectStyles(), 'dsh-job-progress: styles');
477
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-job-progress: dictionaries');
478
+ ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
479
+ name: 'conversation.session.header.actions',
480
+ id: 'job-progress',
481
+ order: 21,
482
+ locale: NS
483
+ }, JobProgressAction));
484
+ }
485
+
486
+ exports.apply = apply;
487
+ exports.inject = inject;
488
+ return module.exports;
489
+ }
490
+ });