dsh-web-icon-indicator 0.4.2 → 0.5.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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.5.0...v0.5.1) (2026-09-22)
11
+
12
+ ### Fixed
13
+
14
+ * load on DSH 0.1.7 and keep the legacy settings contract ([6e8dbb5](https://github.com/waknow/dsh-web-icon-indicator/commit/6e8dbb5402b9847eecc33029d68316407f767288))
15
+
16
+ ## [0.5.0](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.4.2...v0.5.0) (2026-09-17)
17
+
18
+ ### Added
19
+
20
+ * add GitHub Pages showcase site with live bilingual demos ([92ecd70](https://github.com/waknow/dsh-web-icon-indicator/commit/92ecd705217bf3e3a2f52edb86a05665c4ed5b08))
21
+ * add skills-lock.json to manage skill dependencies ([935ad4c](https://github.com/waknow/dsh-web-icon-indicator/commit/935ad4c90a1ba5e6c969ad21dc52c5fbff516c1e))
22
+ * per-instance default icon colour with a similarity warning ([a673c10](https://github.com/waknow/dsh-web-icon-indicator/commit/a673c109b622a074b4d4018caa104cbfb819f149))
23
+
24
+ ### Fixed
25
+
26
+ * harden host/browser behavior and stop advertising unbakeable settings ([5dddbb8](https://github.com/waknow/dsh-web-icon-indicator/commit/5dddbb87c387c8e85785c1d298453919213492d9))
27
+ * repaint favicon instantly when the tab becomes visible ([cd5dfaf](https://github.com/waknow/dsh-web-icon-indicator/commit/cd5dfafdd79f330f775ea35a0a9163ad7ed838cb))
28
+
29
+ ### Changed
30
+
31
+ * pin the registration-time route keys and sync the workflow docs ([de370c5](https://github.com/waknow/dsh-web-icon-indicator/commit/de370c54d3b0777d028bd6ba871756202496c76d))
32
+
10
33
  ## [0.4.2](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.4.1...v0.4.2) (2026-09-07)
11
34
 
12
35
  ### Fixed
@@ -23,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
46
  ### Changed
24
47
 
25
48
  * document declaring/updating the DSH host requirement (engines.dsh) ([fb568b7](https://github.com/waknow/dsh-web-icon-indicator/commit/fb568b71362b4931712240087213845cfb3b76ee))
49
+
26
50
  ## [0.4.0](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.3.1...v0.4.0) (2026-09-04)
27
51
 
28
52
  ### Added
@@ -33,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
57
 
34
58
  * auto-create GitHub Release after successful npm publish ([02d98e3](https://github.com/waknow/dsh-web-icon-indicator/commit/02d98e3db9f83013a3e7667c6a0848efecb8ef37))
35
59
  * note DSH 0.1.2 support and add a version-support banner ([cd42d2c](https://github.com/waknow/dsh-web-icon-indicator/commit/cd42d2c5b5650800e3620c5806b1b4ac2b25c84f))
60
+
36
61
  ## [0.3.1](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.3.0...v0.3.1) (2026-09-02)
37
62
 
38
63
  ### Fixed
package/README.md CHANGED
@@ -1,36 +1,38 @@
1
1
  # dsh-web-icon-indicator
2
2
 
3
- > 📖 [中文文档](README.zh.md) · [English](README.md) · 📝 [Changelog](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases)
3
+ > 📖 [中文文档](README.zh.md) · [English](README.md) · 📝 [Changelog](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases) · 🎨 [Live demo](https://waknow.github.io/dsh-web-icon-indicator/)
4
4
 
5
5
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
6
6
  [![npm version](https://img.shields.io/npm/v/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
9
9
 
10
- > **⚠️ DSH version support** — requires **DSH ≥ 0.1.2** (the settings-service API the configuration card uses). Built & verified against **DSH 0.1.2-rc.1**, the highest version currently tested. On **DSH < 0.1.2** the favicon still works, but the settings UI (**Settings Plugins Plugin config**) is unavailable.
10
+ > **⚠️ DSH version support** — requires **DSH ≥ 0.1.2-rc.1**. One bundle serves both settings generations: **modern** ( 0.1.7-alpha.1: exported `Config` schema + `configForms` + `plugins.row.config`) and **legacy** (≤ 0.1.6-alpha.1: `settings.installSection` + `settingsScope` + `settings.plugin.item`). Verified on **DSH 0.1.5-rc.3** and **DSH 0.1.7-alpha.1**; on any older host the favicon still works even if the settings page is not reachable.
11
11
 
12
12
  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.
13
13
 
14
+ > 🎨 **Live demo** — <https://waknow.github.io/dsh-web-icon-indicator/> · see the four states, the multi-agent counter and every effect rendered live in your browser, no install needed. The playground even drives the demo page's own tab favicon, exactly like the plugin does on a DSH page.
15
+
14
16
  ## ✨ What it does
15
17
 
16
18
  - **Live session state on the tab favicon** — the browser-tab icon mirrors `idle` / `running` / `asking` / `done` (aggregate priority: `asking` > `running` > `done` > `idle`), so background tabs tell you at a glance what your agents are doing — including `ask_user_question` prompts and approval / sandbox-escalation waits, which pin the icon to `asking`.
17
19
  - **One SVG, recolored & animated in the browser** — ships a single whale template ([`icons/base.svg`](./icons/base.svg)); every state, color and frame is rendered client-side as a `data:image/svg+xml` URI. No per-color icon files.
18
20
  - **Six built-in effects** — `static`, `blink`, `breath`, `rainbow`, `heartbeat`, `bounce` — all driven by JavaScript, since favicons don't play SVG CSS animations.
19
21
  - **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
- - **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. 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
+ - **Built-in settings UI, zero YAML** — the *Favicon indicator* page edits the whole config with live color-swatch previews and persists it for you (modern hosts: the profile patch; legacy hosts: the profile `settings.yaml` paths below).
23
+ - **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. Returning to a tab repaints immediately: a `visibilitychange` listener fires an instant status fetch, so a state that flipped while the tab was hidden (e.g. the `done` hold expiring) shows at once instead of waiting for the next — possibly throttled — poll tick. 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
24
  - **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
25
 
24
26
  ### 🛠 Configuration UI — how to get there
25
27
 
26
28
  | # | Step |
27
29
  | --- | --- |
28
- | 1 | Open the DSH Web GUI and go to **Settings / 设置**. |
29
- | 2 | In the **Plugins / 插件** tab, open **Plugin config / 插件配置**. |
30
- | 3 | Find the **Favicon indicator / 标签页图标指示器** card. |
31
- | 4 | Expand a state row (`idle` / `running` / `asking` / `done`) to edit **Effect / 特效**, **Colors / 颜色** (each swatch is a native color picker) and **Cycle (ms) / 周期(毫秒)** (shown only for animated states — static states have no cycle); use **Asking hold / 提问驻留** and **Done hold / 完成驻留** for the two timings. |
30
+ | 1 | Open the DSH Web GUI and go to **Settings / 设置** → **Plugins / 插件**. |
31
+ | 2 | On ≥ 0.1.7: open the **dsh-web-icon-indicator** bundle and click **Configure / 配置** on its `dsh-web-icon-indicator` row. On ≤ 0.1.6-alpha.1: open **Plugin config / 插件配置** in that tab. |
32
+ | 3 | The **Favicon indicator / 标签页图标指示器** page opens with the full form. |
33
+ | 4 | Set **Default icon color / 默认图标颜色** for the idle whale (its only knob — idle paints one color and never animates), then expand a state row (`running` / `asking` / `done`) to edit **Effect / 特效**, **Colors / 颜色** (each swatch is a native color picker) and **Cycle (ms) / 周期(毫秒)** (shown only for animated states — static states have no cycle); use **Asking hold / 提问驻留** and **Done hold / 完成驻留** for the two timings. |
32
34
 
33
- Changes are saved through the settings transport into the profile's `settings.yaml` and applied to the running tab within ~1 s — no reload, no restart. See [Configure](#configure) for the full key reference.
35
+ Changes are saved through the settings transport into the profile patch and applied to the running tab within ~1 s — no reload, no restart. See [Configure](#configure) for the full key reference.
34
36
 
35
37
  ## 🎬 Default configuration, visualized
36
38
 
@@ -42,7 +44,7 @@ The four default states, exactly as they appear in the browser tab (the `asking`
42
44
 
43
45
  | State | Default color | Default effect |
44
46
  | --- | --- | --- |
45
- | `idle` | `#1a1a1a` — deep whale | `static` |
47
+ | `idle` | `#1a1a1a` — deep whale (replaceable with `defaultColor`) | `static` |
46
48
  | `running` | `#FACC15` — yellow | `static` |
47
49
  | `asking` | `#E5484D` ⇄ `#FACC15` — red/yellow | `blink` (400 ms) |
48
50
  | `done` | `#22A06B` — green | `static`, stays `doneHoldMs`, then back to `idle` |
@@ -115,15 +117,20 @@ Or drop the directory into `~/.dsh/profiles/web/node_modules/<name>/` and ship a
115
117
 
116
118
  ## Configure
117
119
 
118
- All keys are optional; defaults shown.
120
+ All keys are optional; defaults shown. `statusPath` and `iconPathPrefix` are
121
+ **registration-time** keys: set them in the composition entry only — they are
122
+ baked into the route table and the injected script when the plugin mounts, so
123
+ they are intentionally **not** part of the settings page's live form (they are
124
+ `Config` fields, but not `.volatile()`).
119
125
 
120
126
  | Key | Default | Meaning |
121
127
  | --- | --- | --- |
122
128
  | `iconsDir` | `<package>/icons/` | Directory holding the single `base.svg` |
123
- | `statusPath` | `/dsh-web-icon-status.json` | JSON status endpoint |
124
- | `iconPathPrefix` | `/dsh-web-icon-indicator` | URL prefix `base.svg` is served under |
129
+ | `statusPath` | `/dsh-web-icon-status.json` | JSON status endpoint — **registration-time (composition entry only)** |
130
+ | `iconPathPrefix` | `/dsh-web-icon-indicator` | URL prefix `base.svg` is served under — **registration-time (composition entry only)** |
125
131
  | `askingHoldMs` | `3500` | Minimum visibility of the asking state |
126
132
  | `doneHoldMs` | `5000` | Time the done state stays before falling back to idle |
133
+ | `defaultColor` | *(unset)* | Default icon color (the idle whale's primary) — tell multiple DSH instances apart. Warns when it is too close to another state's color |
127
134
  | `states` | see below | Per-state visual config |
128
135
 
129
136
  Each entry in `states` is one object per state: `{ effect, colors[], speed? }`:
@@ -138,9 +145,15 @@ config:
138
145
  ```
139
146
 
140
147
  - **`effect`** — one of `static | blink | breath | rainbow | heartbeat | bounce`.
141
- - **`colors`** — an **array** of hex colors. `colors[0]` is the primary. Multi-color effects read more entries: `blink` uses `colors[0]`⇄`colors[1]`, `breath` breathes `colors[0]`⇄`colors[1]` (each derives a darker second color if omitted), `rainbow` uses only `colors[0]` as the starting hue.
148
+ - **`colors`** — an **array** of hex colors (`#rgb` / `#rrggbb`; a malformed entry is ignored, and the state's built-in colour applies when none survives). `colors[0]` is the primary. Multi-color effects read more entries: `blink` uses `colors[0]`⇄`colors[1]`, `breath` breathes `colors[0]`⇄`colors[1]` (each derives a darker second color if omitted), `rainbow` uses only `colors[0]` as the starting hue.
142
149
  - **`speed`** — optional per-state cycle length in ms (also the `blink` toggle interval). Default `1200`.
143
150
 
151
+ `idle` is special: its color is the `defaultColor` key, and the settings card
152
+ offers **no per-state entry** for it (one color, no animation, no cycle). A
153
+ `states.idle` entry is still honored when it arrives from the composition entry
154
+ or the settings document's user layer — backward compatibility only; it is
155
+ simply not editable from the page.
156
+
144
157
  Entries are shallow-merged over the defaults, so you can override only a few states. Example:
145
158
 
146
159
  ```yaml
@@ -153,41 +166,118 @@ Entries are shallow-merged over the defaults, so you can override only a few sta
153
166
  done: { effect: heartbeat, colors: ['#2ECC71'] }
154
167
  ```
155
168
 
156
- ### Settings page & `settings.yaml` (DSH ≥ 0.1.2)
157
-
158
- The plugin registers the whole config surface above with the DSH settings
159
- service under the `web-icon-indicator` namespace (a schemastery schema in
160
- `lib/index.js`):
161
-
162
- - **Web GUI:** open **设置 → 插件 → 插件配置** — a *Favicon indicator* card
163
- edits the same keys (asking/done hold, and per-state effect / colors /
164
- cycle), staged and saved through the settings transport. Each state is a
165
- collapsible row showing a color dot and a one-line summary (`blink ·
166
- #E5484D #FACC15 · 400ms`); expanding a row reveals its three fields, and
167
- the colors field previews parsed swatches live.
168
- - **Persistence:** values land in the profile's `settings.yaml` (default
169
- `~/.dsh/settings.yaml`) as a `web-icon-indicator:` section. The composition
170
- entry stays the `base` layer; resolution order is schema defaults
171
- composition entry settings document user layer.
172
- - **No server restart, no tab reload** for settings-card saves: `askingHoldMs` /
173
- `doneHoldMs` apply live host-side, and per-state visual config (effect /
174
- colors / cycle) is synced into the running tab through the status poll within
175
- ~1 s. Only code-level default changes in `lib/index.js` need a tab reload (or
176
- a DSH web rebuild).
169
+ ### Tell multiple instances apart (`defaultColor`)
170
+
171
+ Running several DSH instances at once (different projects, profiles or ports)?
172
+ Give each one its own default icon color and the browser tabs become
173
+ immediately distinguishable — no need to touch the per-state palette:
174
+
175
+ ```yaml
176
+ - id: dsh-web-icon-indicator
177
+ name: 'dsh-web-icon-indicator'
178
+ config:
179
+ defaultColor: '#5B8DEF'
180
+ ```
181
+
182
+ - `defaultColor` is the **idle whale's primary color**. It is folded into
183
+ `states.idle.colors[0]`, so the idle state keeps its effect and any second
184
+ color you configured; the other states keep their signal colors. Unset (the
185
+ default) means "the idle state's own color", i.e. today's behavior.
186
+ - It is a **per-DSH-instance** setting, not per browser tab: every tab of one
187
+ instance shares it, while another instance (its own profile, e.g.
188
+ `dsh web --port 3081`) can use a different color.
189
+ - **Reset** removes your overrides back to the composition entry. When the
190
+ color comes from that entry (the `base` layer, which an `unset` cannot
191
+ reach), the card writes the idle state's own color instead — so "Reset to
192
+ defaults" really returns the icon to the plain whale color, and the entry's
193
+ value stays reachable through the clear-override control.
194
+ - **Similarity warning.** When the default color is perceptually too close to
195
+ another state's color you get a warning — live in the settings card, in the
196
+ host log, and as `warnings` on the status endpoint — but the value is still
197
+ applied (the warning never blocks saving). Distance is **CIE76 ΔE in
198
+ CIELAB**: `ΔE < 25` warns, `ΔE < 12` is reported as nearly identical
199
+ (`ΔE 2.3` is the just-noticeable difference). Every fill a state actually
200
+ paints is considered: the `asking` blink covers both of its colors,
201
+ `breath` its interpolation, and a `rainbow` state is flagged for any
202
+ chromatic default because it sweeps every hue. The shipped `running` and
203
+ `asking` colors deliberately share `#FACC15`, so states are never compared
204
+ with each other — only the default color against them. A malformed value is
205
+ ignored (and reported) rather than painted.
206
+
207
+ ### Settings page (DSH ≥ 0.1.2-rc.1, both settings generations)
208
+
209
+ The plugin exports the whole config surface above as its Cordis `Config`
210
+ schema (a schemastery schema in `lib/index.js`) **and** registers the same
211
+ schema through the legacy settings service when the running host still exposes
212
+ it — the two paths are feature-detected at mount, so one bundle serves both.
213
+ The namespace differs by generation: **≥ 0.1.7** keys every live form by
214
+ **profile entry id**, so it is `dsh-web-icon-indicator` (the row id the bundle
215
+ patch declares); **≤ 0.1.6-alpha.1** uses the plugin-chosen
216
+ `web-icon-indicator`, the same string 0.5.x used, so an existing section keeps
217
+ resolving:
218
+
219
+ - **Web GUI:** open **设置 → 插件**, expand the **dsh-web-icon-indicator**
220
+ bundle and configure its row. The *Favicon indicator* page edits the same
221
+ keys: asking/done hold, the **default icon color** (with a
222
+ live palette preview and the similarity warning), and the per-state effect /
223
+ colors / cycle for `running` / `asking` / `done`. Each of those states is a
224
+ collapsible row whose header shows one **color chip** per state — split in two
225
+ for a multi-color state, so `asking` shows red | yellow at a glance — plus a
226
+ one-line summary of the effect and cycle (`Blink · 400ms`). Clicking a chip
227
+ opens the native color picker, which is where the hex value is shown/typed;
228
+ the card prints no colour codes itself, except in the similarity warning,
229
+ which names the offending hex to be actionable (e.g. `… Running color #FACC15
230
+ (ΔE 0)`). The expanded row lists one chip per
231
+ color the chosen effect actually uses: `blink` / `breath` get a `+` chip while
232
+ there is still room for a second color (it opens on the darker shade the
233
+ browser would derive anyway, and never appends a color the state already has),
234
+ every color after the first can be removed, and a single-color effect shows —
235
+ and saves — only one chip, so an unused second color can never linger
236
+ invisibly. `rainbow` carries no color list: its field and its chip show the
237
+ hue wheel, next to an optional chip for the **starting hue** (the stored
238
+ `colors[0]`). `idle` deliberately gets **no row** — it paints one
239
+ color and never animates, so the default-color field is its whole
240
+ configuration. Everything is staged and saved through the settings transport.
241
+ - **Persistence:** on ≥ 0.1.7 values land in the profile patch
242
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`) under the
243
+ `dsh-web-icon-indicator` row's `config:` block; on ≤ 0.1.6-alpha.1 they land
244
+ in `~/.dsh/settings.yaml` as a `web-icon-indicator:` section. In both cases
245
+ the composition entry stays the `base` layer; resolution order is schema
246
+ defaults → composition entry → user layer.
247
+ - **No server restart, no tab reload** for settings-page saves. On ≥ 0.1.7 a
248
+ live write is committed into the running plugin's config references
249
+ (`loader/volatile-update` — the loader does not restart the plugin); on the
250
+ legacy line the service calls the registered `onChange` hook. Either way
251
+ `askingHoldMs` / `doneHoldMs` apply host-side immediately and per-state visual
252
+ config (effect / colors / cycle) is synced into the running tab through the
253
+ status poll within ~1 s. Only code-level default changes in `lib/index.js`
254
+ need a tab reload (or a DSH web rebuild).
255
+ - **Route paths are not live settings.** `statusPath` / `iconPathPrefix` are
256
+ validated by `Config` but deliberately **not** `.volatile()`, so they are
257
+ absent from the live form: they are baked into the route table and the
258
+ injected script at registration time, and honoring an edit there would point
259
+ the browser at a path the server never serves. They are composition-entry
260
+ only (see the table above); changing them re-applies the plugin.
261
+ - The live surface therefore covers `askingHoldMs`, `doneHoldMs`, `iconsDir`,
262
+ `defaultColor` and `states`. `iconsDir` has no schema default, so it is
263
+ omitted from the resolved form until a user sets it.
177
264
  - The browser half is a hand-written `lib/client.js` (ModuleLoader factory
178
- format — no build step, no runtime deps beyond the shell's `react`). The DSH
179
- client scanner picks a new `dsh.client` declaration up on the next profile
180
- start.
181
- - Deployments without a settings service are unaffected: the plugin falls back
182
- to reading the composition entry exactly as before.
265
+ format — no build step, no runtime deps beyond the shell's `react`). It
266
+ registers on both page slots (`plugins.row.config` gated on the served
267
+ namespace — and the legacy `settings.plugin.item`) and resolves whichever
268
+ settings provider the host exposes (`configForms` or `settingsScope`) at
269
+ render time, so neither is a hard dependency of the module. The DSH client
270
+ scanner picks a new `dsh.client` declaration up on the next profile start.
271
+ - Deployments without a settings service are unaffected: the plugin keeps
272
+ running on the composition entry + schema defaults it was mounted with.
183
273
 
184
274
  ## How it works
185
275
 
186
- - 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).
276
+ - 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 the plugin's exported `Config` schema, which the DSH settings service projects into a live page keyed by the `dsh-web-icon-indicator` entry id (see above).
187
277
  - 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
278
  - `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
279
  - 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 — 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).
280
+ - The browser script polls `/dsh-web-icon-status.json` once a second (the interval is fixed at 1000 ms in the injected script — it is not a config key), 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. Returning to a tab also triggers an immediate status fetch and repaint (`visibilitychange`), so a state that flipped while the tab was hidden shows at once instead of on the next — possibly throttled — poll tick. 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
281
 
192
282
  ## Browser support & known limitations
193
283
 
package/README.zh.md CHANGED
@@ -1,36 +1,38 @@
1
1
  # dsh-web-icon-indicator
2
2
 
3
- > 📖 [English](README.md) · [中文文档](README.zh.md) · 📝 [更新记录](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases)
3
+ > 📖 [English](README.md) · [中文文档](README.zh.md) · 📝 [更新记录](CHANGELOG.md) · [Releases](https://github.com/waknow/dsh-web-icon-indicator/releases) · 🎨 [在线演示](https://waknow.github.io/dsh-web-icon-indicator/)
4
4
 
5
5
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
6
6
  [![npm version](https://img.shields.io/npm/v/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/dsh-web-icon-indicator)](https://www.npmjs.com/package/dsh-web-icon-indicator)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
9
9
 
10
- > **⚠️ DSH 版本支持** —— 需要 **DSH ≥ 0.1.2**(设置卡片所用的 settings 服务 API)。已针对当前最高测试版本 **DSH 0.1.2-rc.1** 构建并验证。在 **DSH < 0.1.2** favicon 仍可用,但设置页(**设置 → 插件 → 插件配置**)不可用。
10
+ > **⚠️ DSH 版本支持** —— 需要 **DSH ≥ 0.1.2-rc.1**。同一份插件同时兼容两代 settings 契约:**新一代**(≥ 0.1.7-alpha.1:导出 `Config` schema + `configForms` + `plugins.row.config`)与**旧一代**(≤ 0.1.6-alpha.1:`settings.installSection` + `settingsScope` + `settings.plugin.item`)。已在 **DSH 0.1.5-rc.3** **DSH 0.1.7-alpha.1** 上验证;更早的宿主上 favicon 仍可用,只是配置页可能不可达。
11
11
 
12
12
  浏览器标签页 favicon 实时反映 DSH 会话状态——`待机` / `运行中` / `提问` / `完成`——让你在标签页置于后台时也能一眼看出是否有会话需要处理。
13
13
 
14
+ > 🎨 **在线演示** — <https://waknow.github.io/dsh-web-icon-indicator/> · 在浏览器里直接体验四种状态、多 Agent 计数与全部特效,无需安装。试玩间还能驱动演示页自身标签页的真实 favicon——正如插件在 DSH 页面里做的那样。
15
+
14
16
  ## ✨ 功能特性
15
17
 
16
18
  - **标签页 favicon 实时反映会话状态** —— 浏览器标签页图标同步 `idle` / `running` / `asking` / `done`(聚合优先级:`asking` > `running` > `done` > `idle`),后台标签页也能一眼看清 agent 们在做什么——包括 `ask_user_question` 提问,以及审批 / 沙箱提权等待(这两种情况会把图标钉在 `asking` 态)。
17
19
  - **单个 SVG,浏览器内上色与动画** —— 只内置一个鲸鱼模板([`icons/base.svg`](./icons/base.svg));每个状态、颜色、每一帧都在客户端渲染为 `data:image/svg+xml` URI,不再有按颜色拆分的图标文件。
18
20
  - **六种内置特效** —— `static`(静止)、`blink`(闪烁)、`breath`(呼吸)、`rainbow`(彩虹)、`heartbeat`(心跳)、`bounce`(跳动),全部由 JavaScript 驱动(favicon 不会播放 SVG CSS 动画)。
19
21
  - **完全可配置、即时生效** —— 每个状态的颜色、特效、周期,以及提问 / 完成驻留时长,改动约 1 秒内同步到已打开的标签页——无需刷新、无需重启。
20
- - **内置配置 UI,无需手写 YAML** —— DSH 设置页里的 *标签页图标指示器* 卡片可编辑整套配置,带实时色块预览,保存后自动写入 `settings.yaml`(路径见下)。
21
- - **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启、后端停止:故障期间标签页图标绝不丢失(还原启动时缓存的原始图标 `data:`-URI 副本,或保留最后一帧插件图标),端点恢复后自动回到实时状态。
22
+ - **内置配置 UI,无需手写 YAML** —— *标签页图标指示器* 页面可编辑整套配置,带实时色块预览并自动持久化(新宿主:profile patch;旧宿主:profile `settings.yaml`,路径见下)。
23
+ - **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启、后端停止:故障期间标签页图标绝不丢失(还原启动时缓存的原始图标 `data:`-URI 副本,或保留最后一帧插件图标),端点恢复后自动回到实时状态。切回前台时会立刻触发一次状态拉取并重绘——后台标签页的定时器会被浏览器节流,轮询可能滞后,所以回到标签页的瞬间就刷新最新状态(比如 `done` 保持期在隐藏期间过期、图标应退回 `idle` 的情况)。
22
24
  - **多 agent 一目了然** —— 当同时有**超过一个**活动 agent(非待机:`asking` / `running` / `done`)时,favicon 从鲸鱼切换为**占满整帧的数字块**,实时显示活动数(上限 `99+`),颜色与动画和该状态下鲸鱼完全一致;活动数回到 0–1 时恢复鲸鱼。(视觉与 [`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道一致。)
23
25
 
24
26
  ### 🛠 配置界面——怎么找到它
25
27
 
26
28
  | # | 步骤 |
27
29
  | --- | --- |
28
- | 1 | 打开 DSH Web GUI,进入 **设置**。 |
29
- | 2 | **插件** 选项卡中,打开 **插件配置**。 |
30
- | 3 | 找到 **标签页图标指示器(Favicon indicator)** 卡片。 |
31
- | 4 | 展开某个状态行(`idle` / `running` / `asking` / `done`),编辑 **特效**、**颜色**(每个色块即原生取色器),以及**周期(毫秒)**——仅动画状态显示,静态状态无周期;用 **提问驻留** / **完成驻留** 调整两个时长。 |
30
+ | 1 | 打开 DSH Web GUI,进入 **设置** → **插件**。 |
31
+ | 2 | 0.1.7:展开 **dsh-web-icon-indicator** bundle,在其 `dsh-web-icon-indicator` 行上点击 **配置**;≤ 0.1.6-alpha.1:在该选项卡里打开 **插件配置**。 |
32
+ | 3 | 打开 **标签页图标指示器(Favicon indicator)** 配置页。 |
33
+ | 4 | 先用 **默认图标颜色** 设置待机鲸鱼的颜色(这是待机唯一的设置项——待机只画一种颜色、不做动画),再展开状态行(`running` / `asking` / `done`)编辑 **特效**、**颜色**(每个色块即原生取色器),以及**周期(毫秒)**——仅动画状态显示,静态状态无周期;用 **提问驻留** / **完成驻留** 调整两个时长。 |
32
34
 
33
- 改动会通过 settings 传输层持久化到 profile `settings.yaml`,约 1 秒内应用到已打开的标签页——无需刷新、无需重启。完整键说明见 [配置](#配置)。
35
+ 改动会通过 settings 传输层持久化到 profile patch,约 1 秒内应用到已打开的标签页——无需刷新、无需重启。完整键说明见 [配置](#配置)。
34
36
 
35
37
  ## 🎬 默认配置,可视化
36
38
 
@@ -42,7 +44,7 @@
42
44
 
43
45
  | 状态 | 颜色(默认) | 特效(默认) |
44
46
  | --- | --- | --- |
45
- | `idle` 待机 | `#1a1a1a`——深色鲸鱼 | `static` |
47
+ | `idle` 待机 | `#1a1a1a`——深色鲸鱼(可用 `defaultColor` 替换) | `static` |
46
48
  | `running` 运行中 | `#FACC15`——黄色 | `static` |
47
49
  | `asking` 提问 | `#E5484D` ⇄ `#FACC15`——红/黄 | `blink`(400ms) |
48
50
  | `done` 完成 | `#22A06B`——绿色 | `static`,保持 `doneHoldMs` 后回到 `idle` |
@@ -110,15 +112,18 @@ dsh plugin --profile web add <路径或tarball>
110
112
 
111
113
  ## 配置
112
114
 
113
- 所有键均可选,默认值如下:
115
+ 所有键均可选,默认值如下。`statusPath` 与 `iconPathPrefix` 是**注册期**键:
116
+ 只能在合成条目(composition entry)里设置——它们在插件挂载时就被烘进路由表与注入
117
+ 脚本,因此刻意**不**进入配置页的实时表单(它们是 `Config` 字段,但非 `.volatile()`)。
114
118
 
115
119
  | 键 | 默认值 | 含义 |
116
120
  | --- | --- | --- |
117
121
  | `iconsDir` | `<package>/icons/` | 单个 `base.svg` 所在目录 |
118
- | `statusPath` | `/dsh-web-icon-status.json` | JSON 状态端点 |
119
- | `iconPathPrefix` | `/dsh-web-icon-indicator` | `base.svg` 的 URL 前缀 |
122
+ | `statusPath` | `/dsh-web-icon-status.json` | JSON 状态端点 —— **注册期(仅合成条目)** |
123
+ | `iconPathPrefix` | `/dsh-web-icon-indicator` | `base.svg` 的 URL 前缀 —— **注册期(仅合成条目)** |
120
124
  | `askingHoldMs` | `3500` | 提问状态的最小保持时长 |
121
125
  | `doneHoldMs` | `5000` | 完成状态保持时长,随后回到 idle |
126
+ | `defaultColor` | *(未设置)* | 默认图标颜色(待机鲸鱼的主色)—— 用于区分多个 DSH 实例;与其它状态颜色过于接近时会告警 |
122
127
  | `states` | 见下 | 每个状态的视觉配置 |
123
128
 
124
129
  `states` 中每个状态是一个对象:`{ effect, colors[], speed? }`:
@@ -133,9 +138,13 @@ config:
133
138
  ```
134
139
 
135
140
  - **`effect`** — 取 `static | blink | breath | rainbow | heartbeat | bounce` 之一。
136
- - **`colors`** — **数组**,多个 hex 颜色。`colors[0]` 为主色。多色特效读取更多项:`blink` 用 `colors[0]`⇄`colors[1]`,`breath` 在 `colors[0]`⇄`colors[1]` 间过渡(缺省时自动推导更深的第二色),`rainbow` 仅用 `colors[0]` 作起始色相。
141
+ - **`colors`** — **数组**,多个 hex 颜色(`#rgb` / `#rrggbb`;非法项会被逐项忽略,全部无效时才回退到该状态的内置颜色)。`colors[0]` 为主色。多色特效读取更多项:`blink` 用 `colors[0]`⇄`colors[1]`,`breath` 在 `colors[0]`⇄`colors[1]` 间过渡(缺省时自动推导更深的第二色),`rainbow` 仅用 `colors[0]` 作起始色相。
137
142
  - **`speed`** — 可选,该状态的周期(ms),也是 `blink` 的切换间隔。默认 `1200`。
138
143
 
144
+ `idle` 比较特殊:它的颜色就是 `defaultColor` 键,设置卡片**不为它提供状态条目**
145
+ (一种颜色、不做动画、也没有周期)。若 `states.idle` 来自合成条目或手写的
146
+ 设置文档的用户层,仍然会被沿用——这属于向后兼容,只是无法在配置页里编辑。
147
+
139
148
  每个状态条目会在默认值之上做浅合并,因此只需覆盖少量状态。示例:
140
149
 
141
150
  ```yaml
@@ -148,35 +157,92 @@ config:
148
157
  done: { effect: heartbeat, colors: ['#2ECC71'] }
149
158
  ```
150
159
 
151
- ### 设置页与 `settings.yaml`(DSH ≥ 0.1.2)
152
-
153
- 插件把上面整套配置注册进 DSH settings 服务,命名空间为 `web-icon-indicator`
154
- (schema 为 `lib/index.js` 中的 schemastery schema):
155
-
156
- - **Web GUI:** 打开 **设置 → 插件 → 插件配置**,会出现 *标签页图标指示器*
157
- 卡片,可编辑同样的键(提问/完成驻留,以及每个状态的特效 / 颜色 / 周期),
158
- 通过 settings 传输层暂存并保存。每个状态是一行可折叠条目,带主色圆点和一行
159
- 摘要(如 `blink · #E5484D ⇄ #FACC15 · 400ms`);展开该行才显示它的三个字段,
160
- 颜色输入框旁会实时预览解析出的色块。
161
- - **持久化:** 值写入 profile 的 `settings.yaml`(默认 `~/.dsh/settings.yaml`)
162
- 的 `web-icon-indicator:` 段。合成条目仍是 `base` 层;解析顺序为 schema 默认值
163
- 合成条目 设置文档用户层。
164
- - **无需重启服务器、无需刷新标签页**即可让设置卡片的修改生效:`askingHoldMs` /
165
- `doneHoldMs` 在主机侧即时生效;各状态的视觉配置(特效 / 颜色 / 周期)会随状态
166
- 轮询同步进正在运行的标签页,约 1 秒内生效。只有改 `lib/index.js` 里的代码级
167
- 默认值才需要重载标签页(或重新构建 DSH Web)。
160
+ ### 区分多个实例(`defaultColor`)
161
+
162
+ 同时开多个 DSH 实例(不同项目 / profile / 端口)时,给每个实例设一个自己的默认图标
163
+ 颜色,浏览器标签页就能一眼区分,不必去改整套状态配色:
164
+
165
+ ```yaml
166
+ - id: dsh-web-icon-indicator
167
+ name: 'dsh-web-icon-indicator'
168
+ config:
169
+ defaultColor: '#5B8DEF'
170
+ ```
171
+
172
+ - `defaultColor` 就是**待机鲸鱼的主色**。它会被折叠进 `states.idle.colors[0]`,
173
+ 因此 idle 仍保留自己配置的特效与第二色,其它状态的颜色语义(黄=运行、红/黄=提问、
174
+ 绿=完成)不受影响;不设置(默认)即等于「沿用 idle 自己的颜色」,行为与之前完全一致。
175
+ - 这是**按 DSH 实例**生效的设置,不是按标签页:同一实例的所有标签页共用它;另一个实例
176
+ (自己的 profile,例如 `dsh web --port 3081`)可以用另一种颜色。
177
+ - **恢复默认**会把你的覆盖清回合成条目。当颜色本来就来自合成条目(`base` 层,
178
+ 用户层的 `unset` 触及不到)时,卡片改为写入 idle 自己的颜色——这样「恢复默认」真的
179
+ 能让图标回到朴素的鲸鱼色,而合成条目里配置的值仍可通过「清除覆盖」一键取回。
180
+ - **相似度告警**:当默认颜色与其它状态颜色在感知上过于接近时,你会收到告警 —— 设置卡片里
181
+ 实时显示(保存前即可见)、host 日志里记录、状态端点 `warnings` 中返回 —— 但该颜色
182
+ 仍然会被应用(告警绝不阻断保存)。度量方式是 **CIELAB 中的 CIE76 ΔE**:`ΔE < 25`
183
+ 告警,`ΔE < 12` 视为几乎相同(ΔE 2.3 是人眼恰可分辨的阈值)。比较覆盖每个状态实际
184
+ 会画出的所有填充:`asking` 闪烁的两种颜色、`breath` 的插值中间色;而 `rainbow`
185
+ 会扫过所有色相,因此任何有色默认色都会被标记。出厂默认中 `running` 与 `asking` 刻意
186
+ 共用 `#FACC15`,所以**状态之间互不比较**,只把默认色与它们逐一比较。格式非法的值会被
187
+ 忽略并上报,而不会被画到图标上。
188
+
189
+ ### 设置页(DSH ≥ 0.1.2-rc.1,兼容两代 settings 契约)
190
+
191
+ 插件把上面整套配置作为 Cordis `Config` schema 导出(`lib/index.js` 中的
192
+ schemastery schema),**并在宿主仍提供旧版服务时**用同一份 schema 走
193
+ `settings.installSection` 注册——两条路径在挂载时按能力探测,因此同一份 bundle
194
+ 两代宿主都能用。命名空间随代际不同:**≥ 0.1.7** 按 **profile entry id** 组织实时
195
+ 表单,即 `dsh-web-icon-indicator`(bundle patch 声明的行 id);**≤ 0.1.6-alpha.1**
196
+ 使用插件自定的 `web-icon-indicator`,与 0.5.x 保持一致,老用户的 section 继续生效:
197
+
198
+ - **Web GUI:** 打开 **设置 → 插件**,展开 **dsh-web-icon-indicator** bundle 并
199
+ 配置它的行,即出现 *标签页图标指示器* 页面,可编辑:提问/完成驻留、
200
+ **默认图标颜色**(带配色一览与相似度告警),以及
201
+ `running` / `asking` / `done` 三个状态各自的特效 / 颜色 / 周期。这三个状态
202
+ 各占一行可折叠条目,行首是一个**色块**——多色状态(asking)会左右分格同时显示
203
+ 红黄两色,旁边是一行「特效 · 周期」摘要(如 `Blink · 400ms`)。点击色块即打开
204
+ 系统取色弹窗(十六进制值在那里显示/输入),卡片本身不打印颜色代码——唯一例外是相似度
205
+ 告警,它会给出具体的 hex 以便定位(例如「与「运行中」的颜色 #FACC15 几乎相同(ΔE 0)」)。
206
+ 展开该行后只列出当前
207
+ 特效**实际会用到**的颜色:`blink` / `breath` 在还有空位时才多出一个 `+` 色块
208
+ (它默认停在浏览器本来就会推导的那个更深的第二色上,且不会重复添加已有颜色),第一个
209
+ 之后的色块都可以删除;单色特效则只显示、也只保存一个色块,不会留下看不见的第二色。
210
+ `rainbow` 不携带颜色列表:字段与色块都显示彩虹色环,色环旁另有一个**可选的起始色相**
211
+ 色块(也就是存下的 `colors[0]`)。`idle` 刻意**不占
212
+ 一行**——它只画一种颜色、不做动画,默认图标颜色就是它的全部配置。所有修改都通过
213
+ settings 传输层暂存并保存。
214
+ - **持久化:** ≥ 0.1.7 写入 profile patch
215
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`)中 `dsh-web-icon-indicator`
216
+ 行的 `config:` 段;≤ 0.1.6-alpha.1 写入 `~/.dsh/settings.yaml` 的
217
+ `web-icon-indicator:` 段。两者都保持合成条目为 `base` 层;解析顺序为
218
+ schema 默认值 → 合成条目 → 用户层。
219
+ - **无需重启服务器、无需刷新标签页**即可让配置页的修改生效:≥ 0.1.7 时实时写入
220
+ 直接提交到运行中插件的配置引用(`loader/volatile-update`——loader 不会重启插件);
221
+ 旧一代则由 settings 服务回调注册的 `onChange`。两种情况 `askingHoldMs` /
222
+ `doneHoldMs` 都在主机侧即时生效;各状态的视觉配置(特效 / 颜色 / 周期)会随状态
223
+ 轮询同步进正在运行的标签页,约 1 秒内生效。只有改 `lib/index.js` 里的代码级默认值
224
+ 才需要重载标签页(或重新构建 DSH Web)。
225
+ - **路由路径不是实时设置项。** `statusPath` / `iconPathPrefix` 由 `Config` 校验,但
226
+ 刻意**不加** `.volatile()`,因此不出现在实时表单里:它们已被烘进路由表与注入脚本,
227
+ 在那里生效只会让浏览器去请求服务器根本没有提供的路径。它们只存在于合成条目
228
+ (见上方表格),改动会重新挂载插件。
229
+ - 因此实时设置面覆盖 `askingHoldMs`、`doneHoldMs`、`iconsDir`、`defaultColor`
230
+ 与 `states`。`iconsDir` 没有 schema 默认值,用户未设置时不会出现在解析后的表单中。
168
231
  - 浏览器半区是手写的 `lib/client.js`(ModuleLoader factory 格式——无构建步骤、
169
- 无额外运行期依赖,仅用 shell 自带的 `react`)。DSH 客户端扫描器会在下次启动
170
- profile 时识别新的 `dsh.client` 声明。
171
- - 未组合 settings 服务的部署不受影响:插件回退到直接读取合成条目,行为与之前完全一致。
232
+ 无额外运行期依赖,仅用 shell 自带的 `react`)。它同时往两代页面插槽注册
233
+ (`plugins.row.config`——按「Host 是否提供该命名空间」门控;以及旧的
234
+ `settings.plugin.item`),并在渲染时按宿主实际提供的服务解析表单
235
+ (`configForms` 或 `settingsScope`),因此两者都不是模块的硬依赖。DSH 客户端
236
+ 扫描器会在下次启动 profile 时识别新的 `dsh.client` 声明。
237
+ - 未组合 settings 服务的部署不受影响:插件继续使用挂载时的合成条目 + schema 默认值。
172
238
 
173
239
  ## 实现原理
174
240
 
175
- - Host 插件 + 一个小型浏览器半区:在现有 `webServer` 上注册路由——状态 JSON 端点、静态 `/dsh-web-icon-indicator/base.svg`(鲸鱼模板),以及一个 `tapIndex` 向每个 `index.html` 注入小段浏览器脚本。整套配置已注册进 DSH settings 服务(`web-icon-indicator` 命名空间)用于校验、持久化与设置页卡片(见上)。
241
+ - Host 插件 + 一个小型浏览器半区:在现有 `webServer` 上注册路由——状态 JSON 端点、静态 `/dsh-web-icon-indicator/base.svg`(鲸鱼模板),以及一个 `tapIndex` 向每个 `index.html` 注入小段浏览器脚本。整套配置就是插件导出的 `Config` schema,DSH settings 服务按 `dsh-web-icon-indicator` entry id 把它投射为实时配置页(见上)。
176
242
  - 状态按 `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
243
  - `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
178
244
  - 权限 / **沙箱拦截**等待同样会显示为 `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 重启 / 后端停止**:启动时会把原始 favicon 缓存为离线安全的 `data:`-URI 副本,请求失败时还原该副本(副本未取到则保留最后一帧插件图标)——绝不写回原始的服务端 URL(后端停止时它恰恰不可达,写回正是「图标丢失」的根因);每个 tick 持续重试,端点恢复后第一个成功轮询即换回实时图标(SPA 原地重连,无需手动刷新)。
245
+ - 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`(轮询间隔在注入脚本里固定为 1000 ms,不是配置项),首次获取 `base.svg`,然后每个 `requestAnimationFrame` 周期把 favicon 重建为 `data:image/svg+xml,…` URI——把 `__COLOR__` 占位符替换为状态配置的颜色,并应用该状态配置的特效。状态响应还会携带当前的每状态视觉配置,因此设置保存后约 1 秒内(下一个轮询 tick)即同步到已打开的标签页,无需刷新。浏览器不会播放 SVG favicon 的 CSS 动画,所以一切动画都由 JS 驱动。由于浏览器在**隐藏(后台)标签页会暂停 `requestAnimationFrame`**,轮询还会为动画态补绘一帧按墙钟时间计算的画面——后台标签页保持粗粒度动画(约每 1 秒)而不会冻结,切回前台后恢复满速动画。回到前台时还会通过 `visibilitychange` 立即触发一次状态拉取并重绘——后台定时器会被节流,轮询可能滞后,所以切回标签页的瞬间就能看到最新状态(例如隐藏期间 `done` 保持期已过、图标应退回 `idle`)。轮询还能**扛住 host 重启 / 后端停止**:启动时会把原始 favicon 缓存为离线安全的 `data:`-URI 副本,请求失败时还原该副本(副本未取到则保留最后一帧插件图标)——绝不写回原始的服务端 URL(后端停止时它恰恰不可达,写回正是「图标丢失」的根因);每个 tick 持续重试,端点恢复后第一个成功轮询即换回实时图标(SPA 原地重连,无需手动刷新)。
180
246
 
181
247
  ## 浏览器支持与已知限制
182
248