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 +6 -0
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +55 -7
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
384
|
-
// poll:
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
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.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"dsh": ">=0.1.2-rc.1"
|