dsh-web-icon-indicator 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.2](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.4.1...v0.4.2) (2026-09-07)
11
+
12
+ ### Fixed
13
+
14
+ * offline-safe restore keeps the tab icon alive when the DSH host stops ([217f741](https://github.com/waknow/dsh-web-icon-indicator/commit/217f7412bc7f1e2214bd50877b542e5d49257fd4))
15
+
10
16
  ## [0.4.1](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.4.0...v0.4.1) (2026-09-04)
11
17
 
12
18
  ### Added
package/README.md CHANGED
@@ -18,7 +18,7 @@ Browser tab favicon reflects the current DSH session state — `idle` / `running
18
18
  - **Six built-in effects** — `static`, `blink`, `breath`, `rainbow`, `heartbeat`, `bounce` — all driven by JavaScript, since favicons don't play SVG CSS animations.
19
19
  - **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.
20
20
  - **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).
21
- - **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.
21
+ - **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. When the backend is stopped, the tab never loses its icon: the outage restores the shell's own favicon from an offline-safe `data:`-URI copy (or keeps the last painted frame), and the live icon returns on the first successful poll.
22
22
  - **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).)
23
23
 
24
24
  ### 🛠 Configuration UI — how to get there
@@ -187,7 +187,7 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
187
187
  - 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.
188
188
  - `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.
189
189
  - 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`.
190
- - 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).
190
+ - 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 — and a stopped backend never blanks the tab: at startup the script caches an offline-safe `data:`-URI copy of the original favicon, and on a fetch failure it restores that copy (or, if none could be captured, keeps the last painted frame) — it never writes the original server URL back, which would be unreachable exactly while the host is down. It retries every tick, and the live icon returns on the first successful poll (the SPA reconnects in place, so no manual refresh is needed).
191
191
 
192
192
  ## Browser support & known limitations
193
193
 
package/README.zh.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - **六种内置特效** —— `static`(静止)、`blink`(闪烁)、`breath`(呼吸)、`rainbow`(彩虹)、`heartbeat`(心跳)、`bounce`(跳动),全部由 JavaScript 驱动(favicon 不会播放 SVG CSS 动画)。
19
19
  - **完全可配置、即时生效** —— 每个状态的颜色、特效、周期,以及提问 / 完成驻留时长,改动约 1 秒内同步到已打开的标签页——无需刷新、无需重启。
20
20
  - **内置配置 UI,无需手写 YAML** —— DSH 设置页里的 *标签页图标指示器* 卡片可编辑整套配置,带实时色块预览,保存后自动写入 `settings.yaml`(路径见下)。
21
- - **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启,图标自动恢复。
21
+ - **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启、后端停止:故障期间标签页图标绝不丢失(还原启动时缓存的原始图标 `data:`-URI 副本,或保留最后一帧插件图标),端点恢复后自动回到实时状态。
22
22
  - **多 agent 一目了然** —— 当同时有**超过一个**活动 agent(非待机:`asking` / `running` / `done`)时,favicon 从鲸鱼切换为**占满整帧的数字块**,实时显示活动数(上限 `99+`),颜色与动画和该状态下鲸鱼完全一致;活动数回到 0–1 时恢复鲸鱼。(视觉与 [`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道一致。)
23
23
 
24
24
  ### 🛠 配置界面——怎么找到它
@@ -176,7 +176,7 @@ config:
176
176
  - 状态按 `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 在忙。
177
177
  - `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
178
178
  - 权限 / **沙箱拦截**等待同样会显示为 `asking`:当 agent 命中沙箱拒绝并请求提权(`sandbox_permissions` + `justification`),或其他工具需要征得同意时,审批服务会先写入一条 `approval/asked` 会话事件并阻塞 agent,直到你做出决定。插件监听 `session/event`(并以实时会话日志的权威折叠作为兜底)在整个等待期间将会话置为 `asking` 状态,收到 `approval/decided` 后清除。
179
- - 浏览器脚本每秒轮询 `/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 原地重连,无需手动刷新图标即可恢复)。
179
+ - 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`,首次获取 `base.svg`,然后每个 `requestAnimationFrame` 周期把 favicon 重建为 `data:image/svg+xml,…` URI——把 `__COLOR__` 占位符替换为状态配置的颜色,并应用该状态配置的特效。状态响应还会携带当前的每状态视觉配置,因此设置保存后约 1 秒内(下一个轮询 tick)即同步到已打开的标签页,无需刷新。浏览器不会播放 SVG favicon 的 CSS 动画,所以一切动画都由 JS 驱动。由于浏览器在**隐藏(后台)标签页会暂停 `requestAnimationFrame`**,轮询还会为动画态补绘一帧按墙钟时间计算的画面——后台标签页保持粗粒度动画(约每 1 秒)而不会冻结,切回前台后恢复满速动画。轮询还能**扛住 host 重启 / 后端停止**:启动时会把原始 favicon 缓存为离线安全的 `data:`-URI 副本,请求失败时还原该副本(副本未取到则保留最后一帧插件图标)——绝不写回原始的服务端 URL(后端停止时它恰恰不可达,写回正是「图标丢失」的根因);每个 tick 持续重试,端点恢复后第一个成功轮询即换回实时图标(SPA 原地重连,无需手动刷新)。
180
180
 
181
181
  ## 浏览器支持与已知限制
182
182
 
package/lib/index.js CHANGED
@@ -143,6 +143,7 @@ const INJECTED_SCRIPT = `
143
143
  var BIG_NUM_MIN = 2; // show the full-frame number while more than one agent is active
144
144
  var BIG_NUM_RX = 11; // count-block corner radius (demo/badge.html bigNum channel)
145
145
  var ANIM_START = null; // wall-clock start of the current loop (hidden-tab fallback)
146
+ var FAILED = false; // status poll currently failing (restore once per outage)
146
147
  var STATUS_PATH = "__STATUS_PATH__";
147
148
  var BASE_PATH = "__BASE_PATH__";
148
149
  var CFG = __CFG__; // { states: { idle:{effect,colors,speed}, running:…, asking:…, done:… } }
@@ -155,13 +156,51 @@ const INJECTED_SCRIPT = `
155
156
  // Whale geometry (used as the transform pivot for scale/translate effects).
156
157
  var CX = 27.889625, CY = 24.952640;
157
158
 
159
+ // Capture the shell's own favicon ONCE, together with an offline-safe
160
+ // data-URI copy of it. The copy matters: while the DSH host is stopped
161
+ // the original href (a URL served by that same host) is unreachable, so
162
+ // writing it back would blank the tab — the "backend stopped, icon lost"
163
+ // bug. data: URIs render without any network, so restore() below only
164
+ // ever writes those.
158
165
  function captureOriginal() {
166
+ if (ORIGINAL) return;
159
167
  var link = document.querySelector("link[rel='icon']");
160
- if (link && ORIGINAL === null) ORIGINAL = { href: link.getAttribute("href") || "", type: link.getAttribute("type") || "" };
168
+ if (!link) return;
169
+ var href = link.getAttribute("href") || "";
170
+ ORIGINAL = { href: href, type: link.getAttribute("type") || "", data: null };
171
+ if (/^data:/i.test(href)) { ORIGINAL.data = href; return; }
172
+ // Best effort: fetch the original icon and convert it to a data: URI
173
+ // (any image type — blob -> FileReader). Environments without
174
+ // Blob/FileReader, or a failed fetch, leave data null; restore() then
175
+ // keeps the last plugin frame instead of risking a dead URL.
176
+ try {
177
+ if (!href || typeof Blob === "undefined" || typeof FileReader === "undefined") return;
178
+ fetch(href)
179
+ .then(function (r) { if (!r.ok) throw new Error("orig " + r.status); return r.blob(); })
180
+ .then(function (blob) {
181
+ return new Promise(function (resolve, reject) {
182
+ var fr = new FileReader();
183
+ fr.onload = function () { resolve(String(fr.result)); };
184
+ fr.onerror = function () { reject(fr.error || new Error("read failed")); };
185
+ fr.readAsDataURL(blob);
186
+ });
187
+ })
188
+ .then(function (uri) {
189
+ if (ORIGINAL && uri && uri.lastIndexOf("data:", 0) === 0) ORIGINAL.data = uri;
190
+ })
191
+ .catch(function () {});
192
+ } catch (e) {}
161
193
  }
194
+ // Offline-safe restore: prefer the cached data-URI copy of the original
195
+ // icon, then the original href when it is itself a data: URI. With
196
+ // neither available, keep the current frame (also a data: URI) — never
197
+ // write a server URL here, it is unreachable exactly when this runs.
162
198
  function restore() {
163
199
  var link = document.querySelector("link[rel='icon']");
164
- if (link && ORIGINAL) { link.setAttribute("href", ORIGINAL.href); if (ORIGINAL.type) link.setAttribute("type", ORIGINAL.type); }
200
+ if (!link || !ORIGINAL) return;
201
+ var uri = ORIGINAL.data || (/^data:/i.test(ORIGINAL.href) ? ORIGINAL.href : null);
202
+ if (!uri) return;
203
+ try { link.setAttribute("href", uri); if (ORIGINAL.type) link.setAttribute("type", ORIGINAL.type); } catch (e) {}
165
204
  }
166
205
  function linkEl() { return document.querySelector("link[rel='icon']"); }
167
206
  function setHref(uri) {
@@ -379,12 +418,21 @@ const INJECTED_SCRIPT = `
379
418
  // known count (initial 0 → whale only).
380
419
  if (typeof j.active === "number") ACTIVE = j.active;
381
420
  apply(j.state || "idle");
421
+ FAILED = false; // back online — the next outage may restore again
382
422
  })
383
- // Transient failures (host restarting, network blip) must NOT kill the
384
- // poll: restore the original icon and retry on the next tick. The SPA
385
- // reconnects in place, so the tab survives a host restart — the icon
386
- // has to come back on its own once the endpoint returns.
387
- .catch(function () { stopAnim(); restore(); });
423
+ // Transient failures (host stopped/restarting, network blip) must NOT
424
+ // kill the poll: swap the shell's own icon back in ONCE per outage via
425
+ // the offline-safe restore() and keep retrying every tick. The SPA
426
+ // reconnects in place, so the live icon returns by itself once the
427
+ // endpoint answers again. restore() only ever writes data: URIs —
428
+ // never the original server URL, which is unreachable exactly while
429
+ // the host is down — so the tab cannot lose its icon mid-outage.
430
+ .catch(function () {
431
+ stopAnim();
432
+ if (FAILED) return; // already restored for this outage
433
+ FAILED = true;
434
+ restore();
435
+ });
388
436
  }
389
437
  captureOriginal();
390
438
  TIMER = setInterval(poll, 1000);
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.1",
4
+ "version": "0.4.2",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "dsh": ">=0.1.2-rc.1"