dsh-web-icon-indicator 0.2.3 → 0.3.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/CHANGELOG.md +11 -0
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/lib/index.js +113 -17
- package/lib/types/index.d.ts +8 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.2.3...v0.3.0) (2026-08-31)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
* show full-frame active count while multiple agents run ([d287620](https://github.com/waknow/dsh-web-icon-indicator/commit/d287620a6d66a6aa0b4ce8580adad77908ecdaab))
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
* add npm version/downloads/license badges to both READMEs ([e3dd298](https://github.com/waknow/dsh-web-icon-indicator/commit/e3dd29806413ea5f33dd36077caa340c24e8c70c))
|
|
19
|
+
* document automated release flow with commit-and-tag-version ([1754216](https://github.com/waknow/dsh-web-icon-indicator/commit/1754216c32003a722f619935ebf0ab1a402887f1))
|
|
20
|
+
|
|
10
21
|
## [0.2.3](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.2.2...v0.2.3) (2026-08-24)
|
|
11
22
|
|
|
12
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
> 📖 [中文文档](README.zh.md) · [English](README.md) · 📝 [Changelog](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases)
|
|
4
4
|
|
|
5
5
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-web-icon-indicator)
|
|
7
|
+
[](https://www.npmjs.com/package/dsh-web-icon-indicator)
|
|
8
|
+
[](./LICENSE)
|
|
6
9
|
|
|
7
10
|
Browser tab favicon reflects the current DSH session state — `idle` / `running` / `asking` / `done` — so you can see at a glance whether a session needs your attention, even when the tab is in the background.
|
|
8
11
|
|
|
@@ -14,6 +17,7 @@ Browser tab favicon reflects the current DSH session state — `idle` / `running
|
|
|
14
17
|
- **Fully configurable, applied live** — every state's color, effect and cycle speed, plus the asking/done hold timings, apply to the running tab within ~1 s — no reload, no restart.
|
|
15
18
|
- **Built-in settings UI, zero YAML** — a *Favicon indicator* card in the DSH settings page edits the whole config with live color-swatch previews and persists it to `settings.yaml` for you (path below).
|
|
16
19
|
- **Background-tab & restart-proof** — animated states keep a wall-clock fallback while `requestAnimationFrame` is paused in hidden tabs, and the status poll self-heals across host restarts.
|
|
20
|
+
- **Active-agent count at a glance** — while **more than one** agent is active (non-idle: `asking` / `running` / `done`), the favicon switches from the whale to a **full-frame number block** showing the live count (up to `99+`), colored and animated exactly like the whale would be in that state; back to the whale when 0–1 agents are active. (Same visual language as the *满幅数字* channel in [`demo/badge.html`](./demo/badge.html).)
|
|
17
21
|
|
|
18
22
|
### 🛠 Configuration UI — how to get there
|
|
19
23
|
|
|
@@ -151,7 +155,7 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
|
|
|
151
155
|
## How it works
|
|
152
156
|
|
|
153
157
|
- Host plugin with a small browser half: registers routes on the existing `webServer` — the status JSON endpoint, a static `/dsh-web-icon-indicator/base.svg` (the whale template), and one `tapIndex` that injects a small browser script into every served `index.html`. The config surface is registered with the DSH settings service (`web-icon-indicator` namespace) for validation, persistence, and the settings-page card (see above).
|
|
154
|
-
- Status is aggregated across live `agents.list()` with priority `asking > running > done > idle`. The aggregation runs a `reconcile()` step on every request to detect running → idle transitions, because `agent/status`'s idle delivery is not guaranteed at turn end.
|
|
158
|
+
- Status is aggregated across live `agents.list()` with priority `asking > running > done > idle`. The aggregation runs a `reconcile()` step on every request to detect running → idle transitions, because `agent/status`'s idle delivery is not guaranteed at turn end. The status endpoint also reports `active` — the number of non-idle agents — and while that count is **> 1** the injected script renders a full-frame count block (the *满幅数字* channel of [`demo/badge.html`](./demo/badge.html): a rounded block filled with the same per-frame state color/effect as the whale, bold white count sized 31%–52% of the icon, capped at `99+`) instead of the whale, so the tab shows how many agents are busy at once even in a pinned 16px tab.
|
|
155
159
|
- `ask_user_question` tool calls (via `tools/pre-execute` / `tools/result`) flip the session into `asking` with a configurable minimum-hold so the icon stays visible even when the user answers immediately.
|
|
156
160
|
- Permission / **sandbox-interception** waits are also surfaced as `asking`: when the agent hits a sandbox denial and escalates (`sandbox_permissions` + `justification`), or any other tool asks for approval, the approval service appends an `approval/asked` session event and blocks the agent until you decide. The plugin watches `session/event` (with an authoritative fold over the live session log as a fallback) and pins the session into the `asking` state for that whole wait, clearing it on `approval/decided`.
|
|
157
161
|
- The browser script polls `/dsh-web-icon-status.json` once a second, fetches `base.svg` once, and then on every `requestAnimationFrame` tick rebuilds the favicon as a `data:image/svg+xml,…` URI — replacing the `__COLOR__` placeholder with the state's configured color and applying the state's configured effect. The status response also echoes the current per-state visual config, so a settings save reaches the running tab on the next poll (~1 s) without a reload. Browsers don't play favicon SVG CSS animations, so all motion is JS-driven. Because browsers pause `requestAnimationFrame` in hidden tabs, the poll also repaints a wall-clock frame for animated states, so background tabs keep animating (coarsely) instead of freezing; full-speed animation resumes when the tab is visible again. The poll also survives host restarts: a transient fetch failure restores the original icon and retries on the next tick (the SPA reconnects in place, so the icon comes back without a manual refresh).
|
package/README.zh.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
> 📖 [English](README.md) · [中文文档](README.zh.md) · 📝 [更新记录](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases)
|
|
4
4
|
|
|
5
5
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-web-icon-indicator)
|
|
7
|
+
[](https://www.npmjs.com/package/dsh-web-icon-indicator)
|
|
8
|
+
[](./LICENSE)
|
|
6
9
|
|
|
7
10
|
浏览器标签页 favicon 实时反映 DSH 会话状态——`待机` / `运行中` / `提问` / `完成`——让你在标签页置于后台时也能一眼看出是否有会话需要处理。
|
|
8
11
|
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
- **完全可配置、即时生效** —— 每个状态的颜色、特效、周期,以及提问 / 完成驻留时长,改动约 1 秒内同步到已打开的标签页——无需刷新、无需重启。
|
|
15
18
|
- **内置配置 UI,无需手写 YAML** —— DSH 设置页里的 *标签页图标指示器* 卡片可编辑整套配置,带实时色块预览,保存后自动写入 `settings.yaml`(路径见下)。
|
|
16
19
|
- **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启,图标自动恢复。
|
|
20
|
+
- **多 agent 一目了然** —— 当同时有**超过一个**活动 agent(非待机:`asking` / `running` / `done`)时,favicon 从鲸鱼切换为**占满整帧的数字块**,实时显示活动数(上限 `99+`),颜色与动画和该状态下鲸鱼完全一致;活动数回到 0–1 时恢复鲸鱼。(视觉与 [`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道一致。)
|
|
17
21
|
|
|
18
22
|
### 🛠 配置界面——怎么找到它
|
|
19
23
|
|
|
@@ -145,7 +149,7 @@ config:
|
|
|
145
149
|
## 实现原理
|
|
146
150
|
|
|
147
151
|
- Host 插件 + 一个小型浏览器半区:在现有 `webServer` 上注册路由——状态 JSON 端点、静态 `/dsh-web-icon-indicator/base.svg`(鲸鱼模板),以及一个 `tapIndex` 向每个 `index.html` 注入小段浏览器脚本。整套配置已注册进 DSH settings 服务(`web-icon-indicator` 命名空间)用于校验、持久化与设置页卡片(见上)。
|
|
148
|
-
- 状态按 `agents.list()` 聚合,优先级 `asking > running > done > idle`。每次请求都会执行一次 `reconcile()` 检测 running → idle 的转换,因为 `agent/status` 的 idle
|
|
152
|
+
- 状态按 `agents.list()` 聚合,优先级 `asking > running > done > idle`。每次请求都会执行一次 `reconcile()` 检测 running → idle 的转换,因为 `agent/status` 的 idle 事件在回合结束时并不保证送达。状态端点还会上报 `active`——非待机 agent 数——当该数 **> 1** 时,注入脚本改为渲染占满整帧的数字块([`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道:圆角色块,填充色与鲸鱼同源的逐帧状态色/特效,白色粗体数字约占图标高度 31%–52%,上限 `99+`),而不是鲸鱼,这样即使在 16px 的固定标签页里也能一眼看出同时有几个 agent 在忙。
|
|
149
153
|
- `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
|
|
150
154
|
- 权限 / **沙箱拦截**等待同样会显示为 `asking`:当 agent 命中沙箱拒绝并请求提权(`sandbox_permissions` + `justification`),或其他工具需要征得同意时,审批服务会先写入一条 `approval/asked` 会话事件并阻塞 agent,直到你做出决定。插件监听 `session/event`(并以实时会话日志的权威折叠作为兜底)在整个等待期间将会话置为 `asking` 状态,收到 `approval/decided` 后清除。
|
|
151
155
|
- 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`,首次获取 `base.svg`,然后每个 `requestAnimationFrame` 周期把 favicon 重建为 `data:image/svg+xml,…` URI——把 `__COLOR__` 占位符替换为状态配置的颜色,并应用该状态配置的特效。状态响应还会携带当前的每状态视觉配置,因此设置保存后约 1 秒内(下一个轮询 tick)即同步到已打开的标签页,无需刷新。浏览器不会播放 SVG favicon 的 CSS 动画,所以一切动画都由 JS 驱动。由于浏览器在**隐藏(后台)标签页会暂停 `requestAnimationFrame`**,轮询还会为动画态补绘一帧按墙钟时间计算的画面——后台标签页保持粗粒度动画(约每 1 秒)而不会冻结,切回前台后恢复满速动画。轮询还能**扛住 host 重启**:瞬时请求失败时先还原原始图标,并在下一个 tick 重试(SPA 原地重连,无需手动刷新图标即可恢复)。
|
package/lib/index.js
CHANGED
|
@@ -22,8 +22,16 @@
|
|
|
22
22
|
* session into `asking` while the agent waits on the user for a
|
|
23
23
|
* permission decision (covers sandbox escalations too).
|
|
24
24
|
* - `agent/turn-stopping` flips `running`/`asking` into `done`.
|
|
25
|
+
* - A lost `tools/result` cannot pin `asking` forever: the hold re-arms at
|
|
26
|
+
* most ASK_MAX_REARM times, then force-releases against live status.
|
|
25
27
|
* - `reconcile()` (on every status request) watches `agents.list()` for
|
|
26
28
|
* running→idle transitions as a fallback.
|
|
29
|
+
* - The status endpoint also reports `active` — the number of non-idle
|
|
30
|
+
* agents (asking / running / done). While more than one agent is active
|
|
31
|
+
* the favicon shows that count as a full-frame number block (filled with
|
|
32
|
+
* the aggregate state's color/effect, "满幅数字" per demo/badge.html)
|
|
33
|
+
* instead of the whale; with 0–1 active agents the whale is drawn as
|
|
34
|
+
* before.
|
|
27
35
|
*
|
|
28
36
|
* No Client half: the browser script runs outside the sandbox by being
|
|
29
37
|
* injected into the served index.html, where `document` is freely available.
|
|
@@ -126,7 +134,10 @@ const INJECTED_SCRIPT = `
|
|
|
126
134
|
var ORIGINAL = null;
|
|
127
135
|
var TIMER = null;
|
|
128
136
|
var RAF = null;
|
|
129
|
-
var
|
|
137
|
+
var PREV_KEY = null; // render key (state + "|" + active count) the current loop was started for
|
|
138
|
+
var ACTIVE = 0; // non-idle agent count from the status poll; >= BIG_NUM_MIN switches to the count block
|
|
139
|
+
var BIG_NUM_MIN = 2; // show the full-frame number while more than one agent is active
|
|
140
|
+
var BIG_NUM_RX = 11; // count-block corner radius (demo/badge.html bigNum channel)
|
|
130
141
|
var ANIM_START = null; // wall-clock start of the current loop (hidden-tab fallback)
|
|
131
142
|
var STATUS_PATH = "__STATUS_PATH__";
|
|
132
143
|
var BASE_PATH = "__BASE_PATH__";
|
|
@@ -209,6 +220,30 @@ const INJECTED_SCRIPT = `
|
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
// ---- svg frame builder --------------------------------------------------
|
|
223
|
+
// Full-frame activity count ("满幅数字", per demo/badge.html bigNum
|
|
224
|
+
// channel): while more than one agent is active the whole icon becomes a
|
|
225
|
+
// state-colored rounded block with a bold count — no whale is drawn, so
|
|
226
|
+
// count changes never show the whale through the number. Digit size shrinks
|
|
227
|
+
// by width (31%–52% of the icon height), readable at 16px and in pinned
|
|
228
|
+
// tabs; cap at "99+". The fill comes from the SAME per-frame state effect
|
|
229
|
+
// as the whale, so blink/breath/rainbow animate the block identically;
|
|
230
|
+
// geometric transform effects (heartbeat/bounce) only wrap the whale and
|
|
231
|
+
// are deliberately skipped here — a full-frame rect cannot scale without
|
|
232
|
+
// cropping (mirrors the demo's svgFrame showWhale=false branch).
|
|
233
|
+
function bigNumUri(fill) {
|
|
234
|
+
var text = String(ACTIVE > 99 ? "99+" : ACTIVE);
|
|
235
|
+
var chars = text.length;
|
|
236
|
+
var fs = chars === 1 ? 26 : chars === 2 ? 20 : 15.5;
|
|
237
|
+
var rgb = hexToRgb(fill);
|
|
238
|
+
var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
|
|
239
|
+
var fg = lum > 0.6 ? "#111111" : "#FFFFFF"; // auto-contrast on the state color
|
|
240
|
+
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">' +
|
|
241
|
+
'<rect x="0" y="0" width="50" height="50" rx="' + BIG_NUM_RX + '" fill="' + fill + '"/>' +
|
|
242
|
+
'<text x="25" y="' + (25 + fs * 0.34).toFixed(1) + '" text-anchor="middle" font-family="system-ui, sans-serif" font-size="' + fs + '" font-weight="800" fill="' + fg + '">' + text + '</text>' +
|
|
243
|
+
'</svg>';
|
|
244
|
+
return "data:image/svg+xml," + encodeURIComponent(svg);
|
|
245
|
+
}
|
|
246
|
+
|
|
212
247
|
// Reuses the fetched base template; replaces its __COLOR__ token (and,
|
|
213
248
|
// for scale/translate effects, wraps the whale in a <g transform>).
|
|
214
249
|
function frameUri(fill, effect, t, speed) {
|
|
@@ -259,20 +294,24 @@ const INJECTED_SCRIPT = `
|
|
|
259
294
|
var effect = st.effect || "static";
|
|
260
295
|
var speed = st.speed || DEF_SPEED;
|
|
261
296
|
var cols = (st.colors && st.colors.length) ? st.colors : [DEF_COLOR];
|
|
262
|
-
|
|
297
|
+
var fill = frameColor(cols, effect, t, speed);
|
|
298
|
+
// More than one active agent: the count block replaces the whale frame.
|
|
299
|
+
if (ACTIVE >= BIG_NUM_MIN) return bigNumUri(fill);
|
|
300
|
+
return frameUri(fill, effect, t, speed);
|
|
263
301
|
}
|
|
264
302
|
function stopAnim() { if (RAF) { cancelAnimationFrame(RAF); RAF = null; } }
|
|
265
303
|
function apply(state) {
|
|
266
304
|
var link = linkEl();
|
|
267
305
|
if (!link) return;
|
|
268
306
|
if (state == null) { stopAnim(); return; }
|
|
269
|
-
// Unchanged state: keep the running loop
|
|
270
|
-
// 1 s poll would otherwise restart
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
307
|
+
// Unchanged state AND unchanged activity count: keep the running loop
|
|
308
|
+
// (don't reset the phase) — the 1 s poll would otherwise restart
|
|
309
|
+
// animated effects from phase 0 every second. Browsers PAUSE
|
|
310
|
+
// requestAnimationFrame in hidden tabs, so a background tab would freeze
|
|
311
|
+
// on the last frame: repaint here instead — animated states get a
|
|
312
|
+
// wall-clock frame (coarse ~1 Hz animation), static states repaint
|
|
313
|
+
// (self-heal / pick up changes that happened while hidden).
|
|
314
|
+
if (BASE && state + "|" + ACTIVE === PREV_KEY) {
|
|
276
315
|
if (isStatic(state)) {
|
|
277
316
|
var sframe = frameAt(state, 0);
|
|
278
317
|
if (sframe && link.isConnected) setHref(sframe);
|
|
@@ -282,12 +321,12 @@ const INJECTED_SCRIPT = `
|
|
|
282
321
|
}
|
|
283
322
|
return;
|
|
284
323
|
}
|
|
285
|
-
|
|
324
|
+
PREV_KEY = state + "|" + ACTIVE;
|
|
286
325
|
stopAnim();
|
|
287
326
|
if (!BASE) { // base.svg not loaded yet — fetch once, then start
|
|
288
327
|
fetch(location.origin + BASE_PATH + "?t=" + Date.now(), { cache: "no-store" })
|
|
289
328
|
.then(function (r) { if (!r.ok) throw new Error("base " + r.status); return r.text(); })
|
|
290
|
-
.then(function (txt) { BASE = txt;
|
|
329
|
+
.then(function (txt) { BASE = txt; PREV_KEY = null; apply(state); })
|
|
291
330
|
.catch(function () {});
|
|
292
331
|
return;
|
|
293
332
|
}
|
|
@@ -324,12 +363,19 @@ const INJECTED_SCRIPT = `
|
|
|
324
363
|
if (next === CFG_JSON) return;
|
|
325
364
|
CFG_JSON = next;
|
|
326
365
|
CFG = { states: j.states };
|
|
327
|
-
|
|
366
|
+
PREV_KEY = null; // force a full repaint on the next apply
|
|
328
367
|
}
|
|
329
368
|
function poll() {
|
|
330
369
|
fetch(location.origin + STATUS_PATH, { cache: "no-store" })
|
|
331
370
|
.then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); })
|
|
332
|
-
.then(function (j) {
|
|
371
|
+
.then(function (j) {
|
|
372
|
+
syncCfg(j);
|
|
373
|
+
// Non-idle agent count: >= BIG_NUM_MIN switches the frame to the
|
|
374
|
+
// full-frame number. Older hosts omit the field; keep the last
|
|
375
|
+
// known count (initial 0 → whale only).
|
|
376
|
+
if (typeof j.active === "number") ACTIVE = j.active;
|
|
377
|
+
apply(j.state || "idle");
|
|
378
|
+
})
|
|
333
379
|
// Transient failures (host restarting, network blip) must NOT kill the
|
|
334
380
|
// poll: restore the original icon and retry on the next tick. The SPA
|
|
335
381
|
// reconnects in place, so the tab survives a host restart — the icon
|
|
@@ -439,18 +485,36 @@ export default {
|
|
|
439
485
|
const askDone = new Set(); // agentIds whose ask_user_question tool call already returned
|
|
440
486
|
const askTimers = new Map(); // agentId -> ctx.timer.timeout disposer
|
|
441
487
|
const pendingApprovals = new Set(); // agentIds with an open approval/asked (waiting on the user)
|
|
488
|
+
// Per-agent done-hold disposers, keyed like the other maps. Tracking them
|
|
489
|
+
// explicitly (instead of firing a never-cancelled timeout) lets a state
|
|
490
|
+
// leaving `done` cancel the hold, and `agent/disposed` cancel it too; a
|
|
491
|
+
// done→running→done round-trip no longer stacks two timers that race to
|
|
492
|
+
// truncate the hold early.
|
|
493
|
+
const doneTimers = new Map(); // agentId -> ctx.timer.timeout disposer
|
|
494
|
+
|
|
495
|
+
const cancelDoneHold = (id) => {
|
|
496
|
+
const d = doneTimers.get(id);
|
|
497
|
+
if (d) { try { d(); } catch (e) {} doneTimers.delete(id); }
|
|
498
|
+
};
|
|
442
499
|
|
|
443
500
|
const setState = (id, state) => {
|
|
444
501
|
const prev = states.get(id);
|
|
445
502
|
const now = Date.now();
|
|
446
503
|
if (state === "asking") asking.add(id);
|
|
504
|
+
// Same-state transition: keep `since` and — for done — the already-armed
|
|
505
|
+
// hold untouched (never stack a second timer; never cancel the live one).
|
|
447
506
|
if (prev && prev.state === state) { states.set(id, { state, since: prev.since }); return; }
|
|
507
|
+
// Leaving `done` (to running/idle/asking) cancels any in-flight hold so a
|
|
508
|
+
// stale timer can't later force the state back to idle.
|
|
509
|
+
if (prev && prev.state === "done") cancelDoneHold(id);
|
|
448
510
|
states.set(id, { state, since: now });
|
|
449
511
|
if (state === "done") {
|
|
450
512
|
const handle = ctx.timer.timeout(() => {
|
|
451
513
|
const cur = states.get(id);
|
|
452
514
|
if (cur && cur.state === "done") states.set(id, { state: "idle", since: Date.now() });
|
|
515
|
+
doneTimers.delete(id);
|
|
453
516
|
}, cfg.doneHoldMs);
|
|
517
|
+
doneTimers.set(id, handle);
|
|
454
518
|
ctx.effect(() => handle); // register for cleanup
|
|
455
519
|
}
|
|
456
520
|
};
|
|
@@ -477,13 +541,32 @@ export default {
|
|
|
477
541
|
|
|
478
542
|
// Asking: visible minimum (askingHoldMs). Re-arm timer while user
|
|
479
543
|
// is still answering; clean up to live agent status once returned.
|
|
544
|
+
// A call for a brand-new pin cancels any still-pending timer first, so a
|
|
545
|
+
// fast pre-execute→idle→pre-execute round-trip can't stack two timers.
|
|
546
|
+
// as a lost `tools/result` would otherwise re-arm forever and pin the
|
|
547
|
+
// icon to asking indefinitely.
|
|
548
|
+
const ASK_MAX_REARM = 2; // re-arm cap while waiting on a result
|
|
549
|
+
const askWaits = new Map(); // agentId -> consecutive re-arm count
|
|
480
550
|
const scheduleAskCheck = (id) => {
|
|
481
551
|
const prevHandle = askTimers.get(id);
|
|
482
|
-
if (prevHandle) { try { prevHandle(); } catch (e) {} }
|
|
552
|
+
if (prevHandle) { try { prevHandle(); } catch (e) {} askTimers.delete(id); }
|
|
483
553
|
const handle = ctx.timer.timeout(() => {
|
|
484
554
|
askTimers.delete(id);
|
|
485
|
-
if (!asking.has(id)) return;
|
|
486
|
-
if (!askDone.has(id)) {
|
|
555
|
+
if (!asking.has(id)) { askWaits.delete(id); return; }
|
|
556
|
+
if (!askDone.has(id)) {
|
|
557
|
+
const n = (askWaits.get(id) || 0) + 1;
|
|
558
|
+
if (n <= ASK_MAX_REARM) { askWaits.set(id, n); scheduleAskCheck(id); return; }
|
|
559
|
+
// Result never arrived: force-release the pin against live status so
|
|
560
|
+
// a lost tools/result cannot keep the icon blinking forever.
|
|
561
|
+
askWaits.delete(id);
|
|
562
|
+
asking.delete(id);
|
|
563
|
+
askDone.delete(id);
|
|
564
|
+
const live = agents.get(id);
|
|
565
|
+
if (live) setState(id, live.status === "running" ? "running" : "idle");
|
|
566
|
+
else setState(id, "idle");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
askWaits.delete(id);
|
|
487
570
|
asking.delete(id);
|
|
488
571
|
askDone.delete(id);
|
|
489
572
|
const live = agents.get(id);
|
|
@@ -499,6 +582,7 @@ export default {
|
|
|
499
582
|
if (id != null) {
|
|
500
583
|
asking.add(id);
|
|
501
584
|
askDone.delete(id);
|
|
585
|
+
askWaits.delete(id); // fresh pin restarts the re-arm budget
|
|
502
586
|
setState(id, "asking");
|
|
503
587
|
scheduleAskCheck(id);
|
|
504
588
|
}
|
|
@@ -542,6 +626,8 @@ export default {
|
|
|
542
626
|
askDone.delete(id);
|
|
543
627
|
pendingApprovals.delete(id);
|
|
544
628
|
lastSeen.delete(id);
|
|
629
|
+
askWaits.delete(id);
|
|
630
|
+
cancelDoneHold(id);
|
|
545
631
|
const t = askTimers.get(id);
|
|
546
632
|
if (t) { try { t(); } catch (e) {} askTimers.delete(id); }
|
|
547
633
|
});
|
|
@@ -590,9 +676,16 @@ export default {
|
|
|
590
676
|
reconcile();
|
|
591
677
|
const order = ["asking", "running", "done", "idle"];
|
|
592
678
|
let best = null;
|
|
679
|
+
// Non-idle agent count (asking / running / done): the favicon shows this
|
|
680
|
+
// as a full-frame number block while more than one agent is active.
|
|
681
|
+
let active = 0;
|
|
682
|
+
const counted = new Set();
|
|
593
683
|
for (const a of agents.list()) {
|
|
594
684
|
const id = a.id;
|
|
595
685
|
const s = agentStateOf(id);
|
|
686
|
+
if (s !== "idle") {
|
|
687
|
+
if (!counted.has(id)) { active++; counted.add(id); }
|
|
688
|
+
}
|
|
596
689
|
const rank = order.indexOf(s);
|
|
597
690
|
if (rank === -1) continue;
|
|
598
691
|
if (best === null || rank < best.rank) best = { rank, id, state: s };
|
|
@@ -600,10 +693,13 @@ export default {
|
|
|
600
693
|
for (const [id, rec] of states) {
|
|
601
694
|
if (rec.state === "done") {
|
|
602
695
|
const rank = order.indexOf("done");
|
|
696
|
+
if (!counted.has(id)) active++; // done-hold agents not yet live-non-idle
|
|
603
697
|
if (best === null || rank < best.rank) best = { rank, id, state: "done" };
|
|
604
698
|
}
|
|
605
699
|
}
|
|
606
|
-
return best
|
|
700
|
+
return best
|
|
701
|
+
? { state: best.state, since: (states.get(best.id) || {}).since || Date.now(), active }
|
|
702
|
+
: { state: "idle", since: Date.now(), active };
|
|
607
703
|
};
|
|
608
704
|
|
|
609
705
|
// -- Routes --------------------------------------------------------------
|
package/lib/types/index.d.ts
CHANGED
|
@@ -77,6 +77,14 @@ export interface DshWebIconIndicatorAggregate {
|
|
|
77
77
|
state: DshWebIconStateName;
|
|
78
78
|
/** Epoch millis when that aggregate state was first entered. */
|
|
79
79
|
since: number;
|
|
80
|
+
/**
|
|
81
|
+
* Number of non-idle agents (asking / running / done). While it is > 1 the
|
|
82
|
+
* favicon shows this count as a full-frame number block (in the aggregate
|
|
83
|
+
* state's color/effect) instead of the whale; with 0–1 active agents the
|
|
84
|
+
* whale is drawn. Echoed by the status endpoint. Added in 0.3.x; older
|
|
85
|
+
* browser bundles ignore it.
|
|
86
|
+
*/
|
|
87
|
+
active: number;
|
|
80
88
|
/**
|
|
81
89
|
* Current per-state visual config, echoed by the status endpoint so the
|
|
82
90
|
* injected browser script can apply a settings save within ~1 s (no tab
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-web-icon-indicator",
|
|
3
3
|
"description": "Browser tab favicon reflects the DSH session state: idle / running / asking / done, recolored and animated from one base SVG in the browser (fully configurable colors + effects). · 浏览器标签页 favicon 实时反映 DSH 会话状态:待机 / 运行中 / 提问 / 完成,由单个 base.svg 在浏览器中动态上色与动画(颜色与特效完全可配置)。",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"browser-tab"
|
|
68
68
|
],
|
|
69
69
|
"scripts": {
|
|
70
|
+
"test": "node test/verify.js",
|
|
70
71
|
"release": "commit-and-tag-version",
|
|
71
72
|
"release:patch": "commit-and-tag-version --release-as patch",
|
|
72
73
|
"release:minor": "commit-and-tag-version --release-as minor",
|