dsh-web-icon-indicator 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.3.0...v0.3.1) (2026-09-02)
11
+
12
+ ### Fixed
13
+
14
+ * keep asking icon alive while agent is still running ([f9f4a08](https://github.com/waknow/dsh-web-icon-indicator/commit/f9f4a08c452549f43d379c4f7ddf8525f8674a56))
15
+
16
+ ### Changed
17
+
18
+ * add multi-agent count visualization to READMEs ([4326456](https://github.com/waknow/dsh-web-icon-indicator/commit/4326456d60f4f90bab16a3424f91b9550e25da8a))
19
+ ## [0.3.0](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.2.3...v0.3.0) (2026-08-31)
20
+
21
+ ### Added
22
+
23
+ * show full-frame active count while multiple agents run ([d287620](https://github.com/waknow/dsh-web-icon-indicator/commit/d287620a6d66a6aa0b4ce8580adad77908ecdaab))
24
+
25
+ ### Changed
26
+
27
+ * add npm version/downloads/license badges to both READMEs ([e3dd298](https://github.com/waknow/dsh-web-icon-indicator/commit/e3dd29806413ea5f33dd36077caa340c24e8c70c))
28
+ * document automated release flow with commit-and-tag-version ([1754216](https://github.com/waknow/dsh-web-icon-indicator/commit/1754216c32003a722f619935ebf0ab1a402887f1))
29
+
10
30
  ## [0.2.3](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.2.2...v0.2.3) (2026-08-24)
11
31
 
12
32
  ### 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
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
6
+ [![npm version](https://img.shields.io/npm/v/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
7
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./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
 
@@ -41,6 +45,33 @@ The four default states, exactly as they appear in the browser tab (the `asking`
41
45
  | `asking` | `#E5484D` ⇄ `#FACC15` — red/yellow | `blink` (400 ms) |
42
46
  | `done` | `#22A06B` — green | `static`, stays `doneHoldMs`, then back to `idle` |
43
47
 
48
+ ### Multi-agent, visualized
49
+
50
+ With several agents running at once, the favicon itself becomes the counter:
51
+ while **more than one** agent is active (non-idle: `asking` / `running` / `done`,
52
+ including the short `done` hold), the whale is replaced by a full-frame count
53
+ block showing the live `active` count, filled with the aggregate state's color
54
+ and driven by the same effect — so it keeps blinking / breathing / cycling
55
+ exactly like the whale would. With 0–1 active agents it comes right back to
56
+ the whale.
57
+
58
+ <p align="center">
59
+ <img src="assets/multi-agent-count.svg" width="576" alt="active=0 dark whale, active=1 yellow whale, active=2/3 yellow count blocks, active=3 asking red block (blinks), active=100 shows 99+">
60
+ </p>
61
+
62
+ | `active` (non-idle agents) | Favicon |
63
+ | --- | --- |
64
+ | `0` | dark `idle` whale |
65
+ | `1` | that state's whale (`running` yellow, …) |
66
+ | `2`–`99` | full-frame count block; digit height ≈31–52% of the icon (1 digit = 26, 2 = 20, 3+ = 15.5), readable at 16px and in pinned tabs |
67
+ | `100`+ | `99+` |
68
+
69
+ State priority is unchanged, so `asking` still takes over with its red ⇄ yellow
70
+ 400 ms blink (the count block blinks), `done` flashes its color for
71
+ `doneHoldMs`, and the count refreshes live through the status poll (~1 s).
72
+ Same visual language as the *满幅数字* channel of
73
+ [`demo/badge.html`](./demo/badge.html).
74
+
44
75
  ## ✨ All effects, animated
45
76
 
46
77
  Every preview below is the real whale path, animated the same way the plugin renders it (the previews are self-contained animated SVGs — they play right in your browser):
@@ -151,7 +182,7 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
151
182
  ## How it works
152
183
 
153
184
  - 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.
185
+ - 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
186
  - `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
187
  - 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
188
  - 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
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
6
+ [![npm version](https://img.shields.io/npm/v/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
7
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./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
 
@@ -41,6 +45,28 @@
41
45
  | `asking` 提问 | `#E5484D` ⇄ `#FACC15`——红/黄 | `blink`(400ms) |
42
46
  | `done` 完成 | `#22A06B`——绿色 | `static`,保持 `doneHoldMs` 后回到 `idle` |
43
47
 
48
+ ### 多 agent 可视化
49
+
50
+ 当多个 agent 同时运行时,favicon 本身就是计数器:只要活动 agent **超过一个**
51
+ (非待机:`asking` / `running` / `done`,含短暂的 `done` 保持期),鲸鱼就换成
52
+ 占满整帧的数字块——实时显示 `active` 计数,底色取聚合状态色、特效与鲸鱼完全
53
+ 同源(照常闪烁 / 呼吸 / 彩虹);活动数回到 0–1 时恢复鲸鱼。
54
+
55
+ <p align="center">
56
+ <img src="assets/multi-agent-count.svg" width="576" alt="active=0 深色鲸鱼、active=1 黄色鲸鱼、active=2/3 黄色数字块、active=3 asking 红色块(闪烁)、active=100 显示 99+">
57
+ </p>
58
+
59
+ | `active`(非待机 agent 数) | favicon |
60
+ | --- | --- |
61
+ | `0` | 深色 `idle` 鲸鱼 |
62
+ | `1` | 对应状态的鲸鱼(`running` 黄色,…) |
63
+ | `2`–`99` | 占满整帧的数字块,数字高度约占图标的 31%–52%(1 位 26、2 位 20、3+ 位 15.5)——16px 与固定标签页都可读 |
64
+ | `100`+ | `99+` |
65
+
66
+ 状态优先级不变:`asking` 仍以红 ⇄ 黄 400ms 闪烁接管(闪烁的是数字块),
67
+ `done` 按其颜色驻留 `doneHoldMs`,计数随状态轮询实时刷新(约 1 秒)。
68
+ 视觉与 [`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道一致。
69
+
44
70
  ## ✨ 全部特效,动画演示
45
71
 
46
72
  下面每个预览都是真实的鲸鱼路径,按插件实际渲染方式做动画(预览是自包含的动画 SVG,在浏览器里直接播放):
@@ -145,7 +171,7 @@ config:
145
171
  ## 实现原理
146
172
 
147
173
  - 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 事件在回合结束时并不保证送达。
174
+ - 状态按 `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
175
  - `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
150
176
  - 权限 / **沙箱拦截**等待同样会显示为 `asking`:当 agent 命中沙箱拒绝并请求提权(`sandbox_permissions` + `justification`),或其他工具需要征得同意时,审批服务会先写入一条 `approval/asked` 会话事件并阻塞 agent,直到你做出决定。插件监听 `session/event`(并以实时会话日志的权威折叠作为兜底)在整个等待期间将会话置为 `asking` 状态,收到 `approval/decided` 后清除。
151
177
  - 浏览器脚本每秒轮询 `/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 原地重连,无需手动刷新图标即可恢复)。
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="576" height="116" viewBox="0 0 576 116" role="img" aria-label="Multi-agent favicon: whale below 2 active agents, full-frame count block at 2+ (asking blinks red, 100 shows 99+)"><style>.chip{fill:#ffffff;stroke:#d0d7de;stroke-width:1.5}.lbl{font:600 11px system-ui,-apple-system,Segoe UI,sans-serif;fill:#59636e;text-anchor:middle}@keyframes ask{0%,49.9%{fill:#E5484D}50%,100%{fill:#FACC15}}.ask{animation:ask .8s steps(1) infinite}</style><rect class="chip" x="8" y="8" width="80" height="80" rx="20"/><g transform="translate(23,23)"><path id="p0" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#1a1a1a"/></g><text class="lbl" x="48" y="104">0 · idle</text><rect class="chip" x="104" y="8" width="80" height="80" rx="20"/><g transform="translate(119,23)"><path id="p1" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#FACC15"/></g><text class="lbl" x="144" y="104">1 · running</text><rect class="chip" x="200" y="8" width="80" height="80" rx="20"/><rect class="" x="215" y="23" width="50" height="50" rx="11" fill="#FACC15"/><text x="240" y="56.8" text-anchor="middle" font-family="system-ui,sans-serif" font-size="26" font-weight="800" fill="#111111">2</text><text class="lbl" x="240" y="104">2 · running</text><rect class="chip" x="296" y="8" width="80" height="80" rx="20"/><rect class="" x="311" y="23" width="50" height="50" rx="11" fill="#FACC15"/><text x="336" y="56.8" text-anchor="middle" font-family="system-ui,sans-serif" font-size="26" font-weight="800" fill="#111111">3</text><text class="lbl" x="336" y="104">3 · running</text><rect class="chip" x="392" y="8" width="80" height="80" rx="20"/><rect class="ask" x="407" y="23" width="50" height="50" rx="11" fill="#E5484D"/><text x="432" y="56.8" text-anchor="middle" font-family="system-ui,sans-serif" font-size="26" font-weight="800" fill="#FFFFFF">3</text><text class="lbl" x="432" y="104">3 · asking</text><rect class="chip" x="488" y="8" width="80" height="80" rx="20"/><rect class="" x="503" y="23" width="50" height="50" rx="11" fill="#FACC15"/><text x="528" y="53.3" text-anchor="middle" font-family="system-ui,sans-serif" font-size="15.5" font-weight="800" fill="#111111">99+</text><text class="lbl" x="528" y="104">100 · running</text></svg>
package/lib/index.js CHANGED
@@ -22,8 +22,17 @@
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 either: the hold keeps
26
+ * re-arming while the agent is still `running` (the user may be deciding),
27
+ * and force-releases against live status once the turn ends.
25
28
  * - `reconcile()` (on every status request) watches `agents.list()` for
26
29
  * running→idle transitions as a fallback.
30
+ * - The status endpoint also reports `active` — the number of non-idle
31
+ * agents (asking / running / done). While more than one agent is active
32
+ * the favicon shows that count as a full-frame number block (filled with
33
+ * the aggregate state's color/effect, "满幅数字" per demo/badge.html)
34
+ * instead of the whale; with 0–1 active agents the whale is drawn as
35
+ * before.
27
36
  *
28
37
  * No Client half: the browser script runs outside the sandbox by being
29
38
  * injected into the served index.html, where `document` is freely available.
@@ -126,7 +135,10 @@ const INJECTED_SCRIPT = `
126
135
  var ORIGINAL = null;
127
136
  var TIMER = null;
128
137
  var RAF = null;
129
- var PREV_STATE = null; // state the current animation loop was started for
138
+ var PREV_KEY = null; // render key (state + "|" + active count) the current loop was started for
139
+ var ACTIVE = 0; // non-idle agent count from the status poll; >= BIG_NUM_MIN switches to the count block
140
+ var BIG_NUM_MIN = 2; // show the full-frame number while more than one agent is active
141
+ var BIG_NUM_RX = 11; // count-block corner radius (demo/badge.html bigNum channel)
130
142
  var ANIM_START = null; // wall-clock start of the current loop (hidden-tab fallback)
131
143
  var STATUS_PATH = "__STATUS_PATH__";
132
144
  var BASE_PATH = "__BASE_PATH__";
@@ -209,6 +221,30 @@ const INJECTED_SCRIPT = `
209
221
  }
210
222
 
211
223
  // ---- svg frame builder --------------------------------------------------
224
+ // Full-frame activity count ("满幅数字", per demo/badge.html bigNum
225
+ // channel): while more than one agent is active the whole icon becomes a
226
+ // state-colored rounded block with a bold count — no whale is drawn, so
227
+ // count changes never show the whale through the number. Digit size shrinks
228
+ // by width (31%–52% of the icon height), readable at 16px and in pinned
229
+ // tabs; cap at "99+". The fill comes from the SAME per-frame state effect
230
+ // as the whale, so blink/breath/rainbow animate the block identically;
231
+ // geometric transform effects (heartbeat/bounce) only wrap the whale and
232
+ // are deliberately skipped here — a full-frame rect cannot scale without
233
+ // cropping (mirrors the demo's svgFrame showWhale=false branch).
234
+ function bigNumUri(fill) {
235
+ var text = String(ACTIVE > 99 ? "99+" : ACTIVE);
236
+ var chars = text.length;
237
+ var fs = chars === 1 ? 26 : chars === 2 ? 20 : 15.5;
238
+ var rgb = hexToRgb(fill);
239
+ var lum = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
240
+ var fg = lum > 0.6 ? "#111111" : "#FFFFFF"; // auto-contrast on the state color
241
+ var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">' +
242
+ '<rect x="0" y="0" width="50" height="50" rx="' + BIG_NUM_RX + '" fill="' + fill + '"/>' +
243
+ '<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>' +
244
+ '</svg>';
245
+ return "data:image/svg+xml," + encodeURIComponent(svg);
246
+ }
247
+
212
248
  // Reuses the fetched base template; replaces its __COLOR__ token (and,
213
249
  // for scale/translate effects, wraps the whale in a <g transform>).
214
250
  function frameUri(fill, effect, t, speed) {
@@ -259,20 +295,24 @@ const INJECTED_SCRIPT = `
259
295
  var effect = st.effect || "static";
260
296
  var speed = st.speed || DEF_SPEED;
261
297
  var cols = (st.colors && st.colors.length) ? st.colors : [DEF_COLOR];
262
- return frameUri(frameColor(cols, effect, t, speed), effect, t, speed);
298
+ var fill = frameColor(cols, effect, t, speed);
299
+ // More than one active agent: the count block replaces the whale frame.
300
+ if (ACTIVE >= BIG_NUM_MIN) return bigNumUri(fill);
301
+ return frameUri(fill, effect, t, speed);
263
302
  }
264
303
  function stopAnim() { if (RAF) { cancelAnimationFrame(RAF); RAF = null; } }
265
304
  function apply(state) {
266
305
  var link = linkEl();
267
306
  if (!link) return;
268
307
  if (state == null) { stopAnim(); return; }
269
- // Unchanged state: keep the running loop (don't reset the phase) — the
270
- // 1 s poll would otherwise restart animated effects from phase 0 every
271
- // second. Browsers PAUSE requestAnimationFrame in hidden tabs, so a
272
- // background tab would freeze on the last frame: repaint here instead —
273
- // animated states get a wall-clock frame (coarse ~1 Hz animation), static
274
- // states repaint (self-heal / pick up changes that happened while hidden).
275
- if (BASE && state === PREV_STATE) {
308
+ // Unchanged state AND unchanged activity count: keep the running loop
309
+ // (don't reset the phase) — the 1 s poll would otherwise restart
310
+ // animated effects from phase 0 every second. Browsers PAUSE
311
+ // requestAnimationFrame in hidden tabs, so a background tab would freeze
312
+ // on the last frame: repaint here instead animated states get a
313
+ // wall-clock frame (coarse ~1 Hz animation), static states repaint
314
+ // (self-heal / pick up changes that happened while hidden).
315
+ if (BASE && state + "|" + ACTIVE === PREV_KEY) {
276
316
  if (isStatic(state)) {
277
317
  var sframe = frameAt(state, 0);
278
318
  if (sframe && link.isConnected) setHref(sframe);
@@ -282,12 +322,12 @@ const INJECTED_SCRIPT = `
282
322
  }
283
323
  return;
284
324
  }
285
- PREV_STATE = state;
325
+ PREV_KEY = state + "|" + ACTIVE;
286
326
  stopAnim();
287
327
  if (!BASE) { // base.svg not loaded yet — fetch once, then start
288
328
  fetch(location.origin + BASE_PATH + "?t=" + Date.now(), { cache: "no-store" })
289
329
  .then(function (r) { if (!r.ok) throw new Error("base " + r.status); return r.text(); })
290
- .then(function (txt) { BASE = txt; PREV_STATE = null; apply(state); })
330
+ .then(function (txt) { BASE = txt; PREV_KEY = null; apply(state); })
291
331
  .catch(function () {});
292
332
  return;
293
333
  }
@@ -324,12 +364,19 @@ const INJECTED_SCRIPT = `
324
364
  if (next === CFG_JSON) return;
325
365
  CFG_JSON = next;
326
366
  CFG = { states: j.states };
327
- PREV_STATE = null; // force a full repaint on the next apply
367
+ PREV_KEY = null; // force a full repaint on the next apply
328
368
  }
329
369
  function poll() {
330
370
  fetch(location.origin + STATUS_PATH, { cache: "no-store" })
331
371
  .then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); })
332
- .then(function (j) { syncCfg(j); apply(j.state || "idle"); })
372
+ .then(function (j) {
373
+ syncCfg(j);
374
+ // Non-idle agent count: >= BIG_NUM_MIN switches the frame to the
375
+ // full-frame number. Older hosts omit the field; keep the last
376
+ // known count (initial 0 → whale only).
377
+ if (typeof j.active === "number") ACTIVE = j.active;
378
+ apply(j.state || "idle");
379
+ })
333
380
  // Transient failures (host restarting, network blip) must NOT kill the
334
381
  // poll: restore the original icon and retry on the next tick. The SPA
335
382
  // reconnects in place, so the tab survives a host restart — the icon
@@ -439,18 +486,36 @@ export default {
439
486
  const askDone = new Set(); // agentIds whose ask_user_question tool call already returned
440
487
  const askTimers = new Map(); // agentId -> ctx.timer.timeout disposer
441
488
  const pendingApprovals = new Set(); // agentIds with an open approval/asked (waiting on the user)
489
+ // Per-agent done-hold disposers, keyed like the other maps. Tracking them
490
+ // explicitly (instead of firing a never-cancelled timeout) lets a state
491
+ // leaving `done` cancel the hold, and `agent/disposed` cancel it too; a
492
+ // done→running→done round-trip no longer stacks two timers that race to
493
+ // truncate the hold early.
494
+ const doneTimers = new Map(); // agentId -> ctx.timer.timeout disposer
495
+
496
+ const cancelDoneHold = (id) => {
497
+ const d = doneTimers.get(id);
498
+ if (d) { try { d(); } catch (e) {} doneTimers.delete(id); }
499
+ };
442
500
 
443
501
  const setState = (id, state) => {
444
502
  const prev = states.get(id);
445
503
  const now = Date.now();
446
504
  if (state === "asking") asking.add(id);
505
+ // Same-state transition: keep `since` and — for done — the already-armed
506
+ // hold untouched (never stack a second timer; never cancel the live one).
447
507
  if (prev && prev.state === state) { states.set(id, { state, since: prev.since }); return; }
508
+ // Leaving `done` (to running/idle/asking) cancels any in-flight hold so a
509
+ // stale timer can't later force the state back to idle.
510
+ if (prev && prev.state === "done") cancelDoneHold(id);
448
511
  states.set(id, { state, since: now });
449
512
  if (state === "done") {
450
513
  const handle = ctx.timer.timeout(() => {
451
514
  const cur = states.get(id);
452
515
  if (cur && cur.state === "done") states.set(id, { state: "idle", since: Date.now() });
516
+ doneTimers.delete(id);
453
517
  }, cfg.doneHoldMs);
518
+ doneTimers.set(id, handle);
454
519
  ctx.effect(() => handle); // register for cleanup
455
520
  }
456
521
  };
@@ -475,15 +540,35 @@ export default {
475
540
  if (!asking.has(id)) setState(id, "done");
476
541
  });
477
542
 
478
- // Asking: visible minimum (askingHoldMs). Re-arm timer while user
479
- // is still answering; clean up to live agent status once returned.
543
+ // Asking: visible minimum (askingHoldMs). Re-arm timer while the user is
544
+ // still answering; clean up to live agent status once returned.
545
+ // A call for a brand-new pin cancels any still-pending timer first, so a
546
+ // fast pre-execute→idle→pre-execute round-trip can't stack two timers.
547
+ // The pin is kept alive as long as the agent is still `running` — the user
548
+ // may be reading a long question, and a fixed re-arm budget would drop the
549
+ // icon mid-think. It force-releases against live status once the agent
550
+ // leaves `running`, so a lost `tools/result` cannot pin `asking` forever.
480
551
  const scheduleAskCheck = (id) => {
481
552
  const prevHandle = askTimers.get(id);
482
- if (prevHandle) { try { prevHandle(); } catch (e) {} }
553
+ if (prevHandle) { try { prevHandle(); } catch (e) {} askTimers.delete(id); }
483
554
  const handle = ctx.timer.timeout(() => {
484
555
  askTimers.delete(id);
485
556
  if (!asking.has(id)) return;
486
- if (!askDone.has(id)) { scheduleAskCheck(id); return; }
557
+ if (!askDone.has(id)) {
558
+ const live = agents.get(id);
559
+ // Result not yet reported AND the agent is still mid-turn: the user
560
+ // may still be deciding — keep the pin (no re-arm budget; a human
561
+ // question can legitimately outlast many hold windows).
562
+ if (live && live.status === "running") { scheduleAskCheck(id); return; }
563
+ // Result never arrived and the turn ended (agent idle / gone): a lost
564
+ // tools/result cannot keep the icon blinking forever — force-release
565
+ // the pin against live status.
566
+ asking.delete(id);
567
+ askDone.delete(id);
568
+ if (live) setState(id, live.status === "running" ? "running" : "idle");
569
+ else setState(id, "idle");
570
+ return;
571
+ }
487
572
  asking.delete(id);
488
573
  askDone.delete(id);
489
574
  const live = agents.get(id);
@@ -542,6 +627,7 @@ export default {
542
627
  askDone.delete(id);
543
628
  pendingApprovals.delete(id);
544
629
  lastSeen.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 ? { state: best.state, since: (states.get(best.id) || {}).since || Date.now() } : { state: "idle", since: Date.now() };
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 --------------------------------------------------------------
@@ -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.2.3",
4
+ "version": "0.3.1",
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",