dsh-web-icon-indicator 0.5.0 → 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,12 @@ 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
+
10
16
  ## [0.5.0](https://github.com/waknow/dsh-web-icon-indicator/compare/v0.4.2...v0.5.0) (2026-09-17)
11
17
 
12
18
  ### Added
package/README.md CHANGED
@@ -7,7 +7,7 @@
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
 
@@ -19,7 +19,7 @@ Browser tab favicon reflects the current DSH session state — `idle` / `running
19
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.
20
20
  - **Six built-in effects** — `static`, `blink`, `breath`, `rainbow`, `heartbeat`, `bounce` — all driven by JavaScript, since favicons don't play SVG CSS animations.
21
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.
22
- - **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).
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
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.
24
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).)
25
25
 
@@ -27,12 +27,12 @@ Browser tab favicon reflects the current DSH session state — `idle` / `running
27
27
 
28
28
  | # | Step |
29
29
  | --- | --- |
30
- | 1 | Open the DSH Web GUI and go to **Settings / 设置**. |
31
- | 2 | In the **Plugins / 插件** tab, open **Plugin config / 插件配置**. |
32
- | 3 | Find the **Favicon indicator / 标签页图标指示器** card. |
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
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. |
34
34
 
35
- 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.
36
36
 
37
37
  ## 🎬 Default configuration, visualized
38
38
 
@@ -120,7 +120,8 @@ Or drop the directory into `~/.dsh/profiles/web/node_modules/<name>/` and ship a
120
120
  All keys are optional; defaults shown. `statusPath` and `iconPathPrefix` are
121
121
  **registration-time** keys: set them in the composition entry only — they are
122
122
  baked into the route table and the injected script when the plugin mounts, so
123
- they are intentionally **not** part of the settings surface (`settings.yaml`).
123
+ they are intentionally **not** part of the settings page's live form (they are
124
+ `Config` fields, but not `.volatile()`).
124
125
 
125
126
  | Key | Default | Meaning |
126
127
  | --- | --- | --- |
@@ -150,8 +151,8 @@ config:
150
151
  `idle` is special: its color is the `defaultColor` key, and the settings card
151
152
  offers **no per-state entry** for it (one color, no animation, no cycle). A
152
153
  `states.idle` entry is still honored when it arrives from the composition entry
153
- or a hand-written `settings.yaml` — backward compatibility only; it is simply
154
- not editable from the card.
154
+ or the settings document's user layer — backward compatibility only; it is
155
+ simply not editable from the page.
155
156
 
156
157
  Entries are shallow-merged over the defaults, so you can override only a few states. Example:
157
158
 
@@ -183,8 +184,8 @@ immediately distinguishable — no need to touch the per-state palette:
183
184
  color you configured; the other states keep their signal colors. Unset (the
184
185
  default) means "the idle state's own color", i.e. today's behavior.
185
186
  - It is a **per-DSH-instance** setting, not per browser tab: every tab of one
186
- instance shares it, while another instance (its own profile /
187
- `settings.yaml`, e.g. `dsh web --port 3081`) can use a different color.
187
+ instance shares it, while another instance (its own profile, e.g.
188
+ `dsh web --port 3081`) can use a different color.
188
189
  - **Reset** removes your overrides back to the composition entry. When the
189
190
  color comes from that entry (the `base` layer, which an `unset` cannot
190
191
  reach), the card writes the idle state's own color instead — so "Reset to
@@ -203,14 +204,21 @@ immediately distinguishable — no need to touch the per-state palette:
203
204
  with each other — only the default color against them. A malformed value is
204
205
  ignored (and reported) rather than painted.
205
206
 
206
- ### Settings page & `settings.yaml` (DSH ≥ 0.1.2)
207
-
208
- The plugin registers the whole config surface above with the DSH settings
209
- service under the `web-icon-indicator` namespace (a schemastery schema in
210
- `lib/index.js`):
211
-
212
- - **Web GUI:** open **设置 插件 插件配置** a *Favicon indicator* card
213
- edits the same keys: asking/done hold, the **default icon color** (with a
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
214
222
  live palette preview and the similarity warning), and the per-state effect /
215
223
  colors / cycle for `running` / `asking` / `done`. Each of those states is a
216
224
  collapsible row whose header shows one **color chip** per state — split in two
@@ -230,34 +238,42 @@ service under the `web-icon-indicator` namespace (a schemastery schema in
230
238
  `colors[0]`). `idle` deliberately gets **no row** — it paints one
231
239
  color and never animates, so the default-color field is its whole
232
240
  configuration. Everything is staged and saved through the settings transport.
233
- - **Persistence:** values land in the profile's `settings.yaml` (default
234
- `~/.dsh/settings.yaml`) as a `web-icon-indicator:` section. The composition
235
- entry stays the `base` layer; resolution order is schema defaults →
236
- composition entry settings document user layer.
237
- - **No server restart, no tab reload** for settings-card saves: `askingHoldMs` /
238
- `doneHoldMs` apply live host-side, and per-state visual config (effect /
239
- colors / cycle) is synced into the running tab through the status poll within
240
- ~1 s. Only code-level default changes in `lib/index.js` need a tab reload (or
241
- a DSH web rebuild).
242
- - **Route paths are not settings.** `statusPath` / `iconPathPrefix` are
243
- registration-time keys baked into the route table and the injected script, so
244
- they live in the composition entry only (see the table above) and a restart is
245
- required to change them. They are deliberately absent from the settings schema
246
- and from `settings.yaml`: honoring them there would point the browser at a path
247
- the server never serves.
248
- - The settings surface therefore covers `askingHoldMs`, `doneHoldMs`,
249
- `iconsDir` and `states`. `iconsDir` has no schema default, so it is omitted
250
- from the settings document unless a user sets it.
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.
251
264
  - The browser half is a hand-written `lib/client.js` (ModuleLoader factory
252
- format — no build step, no runtime deps beyond the shell's `react`). The DSH
253
- client scanner picks a new `dsh.client` declaration up on the next profile
254
- start.
255
- - Deployments without a settings service are unaffected: the plugin falls back
256
- 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.
257
273
 
258
274
  ## How it works
259
275
 
260
- - 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).
261
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.
262
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.
263
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`.
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
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
 
@@ -19,7 +19,7 @@
19
19
  - **单个 SVG,浏览器内上色与动画** —— 只内置一个鲸鱼模板([`icons/base.svg`](./icons/base.svg));每个状态、颜色、每一帧都在客户端渲染为 `data:image/svg+xml` URI,不再有按颜色拆分的图标文件。
20
20
  - **六种内置特效** —— `static`(静止)、`blink`(闪烁)、`breath`(呼吸)、`rainbow`(彩虹)、`heartbeat`(心跳)、`bounce`(跳动),全部由 JavaScript 驱动(favicon 不会播放 SVG CSS 动画)。
21
21
  - **完全可配置、即时生效** —— 每个状态的颜色、特效、周期,以及提问 / 完成驻留时长,改动约 1 秒内同步到已打开的标签页——无需刷新、无需重启。
22
- - **内置配置 UI,无需手写 YAML** —— DSH 设置页里的 *标签页图标指示器* 卡片可编辑整套配置,带实时色块预览,保存后自动写入 `settings.yaml`(路径见下)。
22
+ - **内置配置 UI,无需手写 YAML** —— *标签页图标指示器* 页面可编辑整套配置,带实时色块预览并自动持久化(新宿主:profile patch;旧宿主:profile `settings.yaml`,路径见下)。
23
23
  - **后台标签页与重启抗性** —— 隐藏标签页中 `requestAnimationFrame` 被暂停时,动画态会按墙钟时间补帧;状态轮询还能扛住 host 重启、后端停止:故障期间标签页图标绝不丢失(还原启动时缓存的原始图标 `data:`-URI 副本,或保留最后一帧插件图标),端点恢复后自动回到实时状态。切回前台时会立刻触发一次状态拉取并重绘——后台标签页的定时器会被浏览器节流,轮询可能滞后,所以回到标签页的瞬间就刷新最新状态(比如 `done` 保持期在隐藏期间过期、图标应退回 `idle` 的情况)。
24
24
  - **多 agent 一目了然** —— 当同时有**超过一个**活动 agent(非待机:`asking` / `running` / `done`)时,favicon 从鲸鱼切换为**占满整帧的数字块**,实时显示活动数(上限 `99+`),颜色与动画和该状态下鲸鱼完全一致;活动数回到 0–1 时恢复鲸鱼。(视觉与 [`demo/badge.html`](./demo/badge.html) 的「满幅数字」通道一致。)
25
25
 
@@ -27,12 +27,12 @@
27
27
 
28
28
  | # | 步骤 |
29
29
  | --- | --- |
30
- | 1 | 打开 DSH Web GUI,进入 **设置**。 |
31
- | 2 | **插件** 选项卡中,打开 **插件配置**。 |
32
- | 3 | 找到 **标签页图标指示器(Favicon indicator)** 卡片。 |
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
33
  | 4 | 先用 **默认图标颜色** 设置待机鲸鱼的颜色(这是待机唯一的设置项——待机只画一种颜色、不做动画),再展开状态行(`running` / `asking` / `done`)编辑 **特效**、**颜色**(每个色块即原生取色器),以及**周期(毫秒)**——仅动画状态显示,静态状态无周期;用 **提问驻留** / **完成驻留** 调整两个时长。 |
34
34
 
35
- 改动会通过 settings 传输层持久化到 profile `settings.yaml`,约 1 秒内应用到已打开的标签页——无需刷新、无需重启。完整键说明见 [配置](#配置)。
35
+ 改动会通过 settings 传输层持久化到 profile patch,约 1 秒内应用到已打开的标签页——无需刷新、无需重启。完整键说明见 [配置](#配置)。
36
36
 
37
37
  ## 🎬 默认配置,可视化
38
38
 
@@ -114,7 +114,7 @@ dsh plugin --profile web add <路径或tarball>
114
114
 
115
115
  所有键均可选,默认值如下。`statusPath` 与 `iconPathPrefix` 是**注册期**键:
116
116
  只能在合成条目(composition entry)里设置——它们在插件挂载时就被烘进路由表与注入
117
- 脚本,因此刻意**不**进入设置面(`settings.yaml`)。
117
+ 脚本,因此刻意**不**进入配置页的实时表单(它们是 `Config` 字段,但非 `.volatile()`)。
118
118
 
119
119
  | 键 | 默认值 | 含义 |
120
120
  | --- | --- | --- |
@@ -143,7 +143,7 @@ config:
143
143
 
144
144
  `idle` 比较特殊:它的颜色就是 `defaultColor` 键,设置卡片**不为它提供状态条目**
145
145
  (一种颜色、不做动画、也没有周期)。若 `states.idle` 来自合成条目或手写的
146
- `settings.yaml`,仍然会被沿用——这属于向后兼容,只是无法在卡片里编辑。
146
+ 设置文档的用户层,仍然会被沿用——这属于向后兼容,只是无法在配置页里编辑。
147
147
 
148
148
  每个状态条目会在默认值之上做浅合并,因此只需覆盖少量状态。示例:
149
149
 
@@ -173,7 +173,7 @@ config:
173
173
  因此 idle 仍保留自己配置的特效与第二色,其它状态的颜色语义(黄=运行、红/黄=提问、
174
174
  绿=完成)不受影响;不设置(默认)即等于「沿用 idle 自己的颜色」,行为与之前完全一致。
175
175
  - 这是**按 DSH 实例**生效的设置,不是按标签页:同一实例的所有标签页共用它;另一个实例
176
- (自己的 profile / `settings.yaml`,例如 `dsh web --port 3081`)可以用另一种颜色。
176
+ (自己的 profile,例如 `dsh web --port 3081`)可以用另一种颜色。
177
177
  - **恢复默认**会把你的覆盖清回合成条目。当颜色本来就来自合成条目(`base` 层,
178
178
  用户层的 `unset` 触及不到)时,卡片改为写入 idle 自己的颜色——这样「恢复默认」真的
179
179
  能让图标回到朴素的鲸鱼色,而合成条目里配置的值仍可通过「清除覆盖」一键取回。
@@ -186,13 +186,18 @@ config:
186
186
  共用 `#FACC15`,所以**状态之间互不比较**,只把默认色与它们逐一比较。格式非法的值会被
187
187
  忽略并上报,而不会被画到图标上。
188
188
 
189
- ### 设置页与 `settings.yaml`(DSH ≥ 0.1.2
189
+ ### 设置页(DSH ≥ 0.1.2-rc.1,兼容两代 settings 契约)
190
190
 
191
- 插件把上面整套配置注册进 DSH settings 服务,命名空间为 `web-icon-indicator`
192
- schema `lib/index.js` 中的 schemastery schema):
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 继续生效:
193
197
 
194
- - **Web GUI:** 打开 **设置 → 插件 插件配置**,会出现 *标签页图标指示器*
195
- 卡片,可编辑:提问/完成驻留、**默认图标颜色**(带配色一览与相似度告警),以及
198
+ - **Web GUI:** 打开 **设置 → 插件**,展开 **dsh-web-icon-indicator** bundle
199
+ 配置它的行,即出现 *标签页图标指示器* 页面,可编辑:提问/完成驻留、
200
+ **默认图标颜色**(带配色一览与相似度告警),以及
196
201
  `running` / `asking` / `done` 三个状态各自的特效 / 颜色 / 周期。这三个状态
197
202
  各占一行可折叠条目,行首是一个**色块**——多色状态(asking)会左右分格同时显示
198
203
  红黄两色,旁边是一行「特效 · 周期」摘要(如 `Blink · 400ms`)。点击色块即打开
@@ -206,27 +211,34 @@ config:
206
211
  色块(也就是存下的 `colors[0]`)。`idle` 刻意**不占
207
212
  一行**——它只画一种颜色、不做动画,默认图标颜色就是它的全部配置。所有修改都通过
208
213
  settings 传输层暂存并保存。
209
- - **持久化:** 值写入 profile `settings.yaml`(默认 `~/.dsh/settings.yaml`)
210
- `web-icon-indicator:` 段。合成条目仍是 `base` 层;解析顺序为 schema 默认值
211
- 合成条目 设置文档用户层。
212
- - **无需重启服务器、无需刷新标签页**即可让设置卡片的修改生效:`askingHoldMs` /
213
- `doneHoldMs` 在主机侧即时生效;各状态的视觉配置(特效 / 颜色 / 周期)会随状态
214
- 轮询同步进正在运行的标签页,约 1 秒内生效。只有改 `lib/index.js` 里的代码级
215
- 默认值才需要重载标签页(或重新构建 DSH Web)。
216
- - **路由路径不是设置项。** `statusPath` / `iconPathPrefix` 是注册期键,已被烘进
217
- 路由表与注入脚本,因此只存在于合成条目(见上方表格),改动需要重启。它们刻意
218
- 不在设置 schema `settings.yaml` 中:若在那里生效,浏览器会去请求服务器根本
219
- 没有提供的路径。
220
- - 因此设置面覆盖 `askingHoldMs`、`doneHoldMs`、`iconsDir` `states`。
221
- `iconsDir` 没有 schema 默认值,用户未设置时不会出现在设置文档中。
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 默认值,用户未设置时不会出现在解析后的表单中。
222
231
  - 浏览器半区是手写的 `lib/client.js`(ModuleLoader factory 格式——无构建步骤、
223
- 无额外运行期依赖,仅用 shell 自带的 `react`)。DSH 客户端扫描器会在下次启动
224
- profile 时识别新的 `dsh.client` 声明。
225
- - 未组合 settings 服务的部署不受影响:插件回退到直接读取合成条目,行为与之前完全一致。
232
+ 无额外运行期依赖,仅用 shell 自带的 `react`)。它同时往两代页面插槽注册
233
+ (`plugins.row.config`——按「Host 是否提供该命名空间」门控;以及旧的
234
+ `settings.plugin.item`),并在渲染时按宿主实际提供的服务解析表单
235
+ (`configForms` 或 `settingsScope`),因此两者都不是模块的硬依赖。DSH 客户端
236
+ 扫描器会在下次启动 profile 时识别新的 `dsh.client` 声明。
237
+ - 未组合 settings 服务的部署不受影响:插件继续使用挂载时的合成条目 + schema 默认值。
226
238
 
227
239
  ## 实现原理
228
240
 
229
- - 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 把它投射为实时配置页(见上)。
230
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 在忙。
231
243
  - `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
232
244
  - 权限 / **沙箱拦截**等待同样会显示为 `asking`:当 agent 命中沙箱拒绝并请求提权(`sandbox_permissions` + `justification`),或其他工具需要征得同意时,审批服务会先写入一条 `approval/asked` 会话事件并阻塞 agent,直到你做出决定。插件监听 `session/event`(并以实时会话日志的权威折叠作为兜底)在整个等待期间将会话置为 `asking` 状态,收到 `approval/decided` 后清除。
package/lib/client.js CHANGED
@@ -2,15 +2,26 @@
2
2
  * Browser half of dsh-web-icon-indicator (dsh.client bundle, ModuleLoader
3
3
  * factory format — hand-written, no build step).
4
4
  *
5
- * Registers one card into the shared "plugin configuration" surface
6
- * (`settings.plugin.item`, keyed by the `web-icon-indicator` settings
7
- * namespace). The card binds the namespace's settings scope, stages edits
8
- * locally, and writes them through `scope.set` / `scope.unset` (validated and
9
- * persisted host-side into the profile's settings.yaml).
5
+ * Registers one configuration page and serves BOTH host generations from one
6
+ * bundle:
10
7
  *
11
- * The tab that dispatches this slot (`dsh-client-ui-settings-plugins`) only
12
- * renders the card while the host serves the namespace, so this bundle is
13
- * inert on deployments that do not compose the host plugin.
8
+ * - DSH 0.1.7 (modern): page slot `plugins.row.config` keyed
9
+ * `<package name>#<row id>`, bound to the profile-entry settings namespace
10
+ * through `configForms` (`ctx.configForms.get(entryId)`).
11
+ * - DSH ≤ 0.1.6-alpha.1 (legacy): page slot `settings.plugin.item` keyed by the
12
+ * legacy `web-icon-indicator` namespace, bound through `settingsScope`.
13
+ *
14
+ * Neither settings provider is declared in `inject` — both are read weakly at
15
+ * render time (`resolveScope`) and the slot registration is attempted for both
16
+ * names (`slots.inject` is a no-op for a slot the host never declares). The
17
+ * card body is generation-agnostic: the modern controller and the legacy scope
18
+ * expose the same snapshot + `mutate` / `unset` face. The card stages edits
19
+ * locally and writes them through that scope (validated, revision-fenced, and
20
+ * persisted host-side).
21
+ *
22
+ * The page is inert on deployments that do not compose the host plugin: on the
23
+ * modern line registration is gated on `configForms.whileServed`, and the
24
+ * legacy tab only dispatches namespaces the host actually serves.
14
25
  */
15
26
  window.__ModuleLoader__.load({
16
27
  id: "dsh-web-icon-indicator",
@@ -26,9 +37,28 @@ window.__ModuleLoader__.load({
26
37
  // Official UI primitives (same source the shell's own plugin cards use,
27
38
  // e.g. dsh-client-ui-settings-plugins): the standard collapse chevron.
28
39
  var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
40
+ // The primitives icon set moved from size-suffixed names (`…Outline14`) to
41
+ // stroke-weight names (`…OutlineRegular` / `…OutlineMedium`); fall back so
42
+ // one bundle keeps working on both host lines.
43
+ var ChevronDownIcon = primitives.IconChevronDownOutlineRegular || primitives.IconChevronDownOutline14;
29
44
 
30
- /** Settings namespace owned by the host plugin (must match lib/index.js). */
31
- var NS = "web-icon-indicator";
45
+ /**
46
+ * Settings namespace owned by the host plugin on the MODERN host line
47
+ * (DSH ≥ 0.1.7): the settings service keys every form by the profile entry
48
+ * id, and the bundle patch in `cordis.patch.yml` inserts exactly one row
49
+ * with this id. Keep it in sync with `SETTINGS_NAMESPACE` in lib/index.js
50
+ * and the row id in the patch.
51
+ */
52
+ var NS = "dsh-web-icon-indicator";
53
+ /**
54
+ * Settings namespace the LEGACY host line (DSH ≤ 0.1.6-alpha.1) serves: the
55
+ * plugin registers it through `settings.installSection`, and this half binds
56
+ * the legacy `settingsScope` to the same string. Also the key the legacy
57
+ * settings tab dispatches in `settings.plugin.item`.
58
+ */
59
+ var LEGACY_NS = "web-icon-indicator";
60
+ /** `plugins.row.config` key: `<bundle package name>#<row id as the patch declares it>`. */
61
+ var ROW_KEY = "dsh-web-icon-indicator#dsh-web-icon-indicator";
32
62
  var LOCALE = "dsh-web-icon-indicator";
33
63
  var STATE_NAMES = ["idle", "running", "asking", "done"];
34
64
  var EFFECT_NAMES = ["static", "blink", "breath", "rainbow", "heartbeat", "bounce"];
@@ -82,8 +112,17 @@ window.__ModuleLoader__.load({
82
112
  bounce: "effectBounce",
83
113
  };
84
114
 
85
- /** Cordis fiber services this browser plugin injects. */
86
- var inject = ["slots", "settingsScope", "locale"];
115
+ /**
116
+ * Cordis fiber services this browser plugin injects.
117
+ *
118
+ * Deliberately only the two services every host line provides: the settings
119
+ * provider itself is read weakly at render time (`resolveScope`), so one
120
+ * bundle serves both the modern `configForms` line (DSH ≥ 0.1.7) and the
121
+ * legacy `settingsScope` line (≤ 0.1.6-alpha.1). A hard `inject` on either
122
+ * name would leave the fiber un-activated on the other line, which is what
123
+ * broke the 0.5.x bundle on 0.1.7.
124
+ */
125
+ var inject = ["slots", "locale"];
87
126
 
88
127
  // ---------------------------------------------------------------------------
89
128
  // Locale copy
@@ -642,12 +681,17 @@ window.__ModuleLoader__.load({
642
681
  return { kind: "set", value: parsed };
643
682
  }
644
683
 
645
- /** Subscribe a React component to the scope's snapshot store. */
684
+ /**
685
+ * Subscribe a React component to the scope's snapshot store. `scope` may be
686
+ * null while the host serves no settings provider for our namespace (or
687
+ * before one appears); the hook count stays identical either way, and a
688
+ * null scope yields a null snapshot that renders nothing.
689
+ */
646
690
  function useScopeSnapshot(scope) {
647
691
  return useSyncExternalStore(
648
- function (listener) { return scope.subscribe(listener); },
649
- function () { return scope.getSnapshot(); },
650
- function () { return scope.getSnapshot(); }
692
+ function (listener) { return scope ? scope.subscribe(listener) : function () {}; },
693
+ function () { return scope ? scope.getSnapshot() : null; },
694
+ function () { return scope ? scope.getSnapshot() : null; }
651
695
  );
652
696
  }
653
697
 
@@ -1050,11 +1094,18 @@ window.__ModuleLoader__.load({
1050
1094
  };
1051
1095
 
1052
1096
  var disabled = !writable || saving;
1097
+ // The Plugins page asks a configuration entry twice: `view: 'summary'`
1098
+ // for the row's one-liner and `view: 'page'` for the body of its own
1099
+ // page (the page already heads it with the plugin's title and
1100
+ // description). A host that renders this card standalone passes no
1101
+ // `view` and keeps the collapsible header.
1102
+ if (props.view === "summary") return t("description");
1103
+ var pageView = props.view === "page";
1053
1104
 
1054
1105
  return createElement(
1055
1106
  "div",
1056
1107
  { style: styles.card },
1057
- createElement(
1108
+ pageView ? null : createElement(
1058
1109
  "button",
1059
1110
  {
1060
1111
  type: "button",
@@ -1073,10 +1124,10 @@ window.__ModuleLoader__.load({
1073
1124
  createElement(
1074
1125
  "span",
1075
1126
  { style: Object.assign({}, styles.chevron, open ? styles.chevronOpen : {}) },
1076
- createElement(primitives.IconChevronDownOutline14, {})
1127
+ createElement(ChevronDownIcon, {})
1077
1128
  )
1078
1129
  ),
1079
- open ? createElement(
1130
+ (open || pageView) ? createElement(
1080
1131
  "div",
1081
1132
  { style: styles.body },
1082
1133
  !writable ? createElement("p", { style: styles.hint }, t("readOnly")) : null,
@@ -1274,23 +1325,81 @@ window.__ModuleLoader__.load({
1274
1325
  // ---------------------------------------------------------------------------
1275
1326
  // Plugin entry
1276
1327
  // ---------------------------------------------------------------------------
1328
+
1329
+ /**
1330
+ * The settings form this card binds, on whichever host line is running.
1331
+ *
1332
+ * - DSH ≥ 0.1.7 exposes `configForms`, one controller per profile entry id
1333
+ * (snapshot + revision-fenced `mutate` / `unset`); the provider owns the
1334
+ * remote write, so this plugin never declares `remote` itself.
1335
+ * - DSH ≤ 0.1.6-alpha.1 exposes `settingsScope`; binding it to the legacy
1336
+ * namespace yields the same snapshot/write face the card already uses.
1337
+ *
1338
+ * Read weakly on purpose: the two services are mutually exclusive, and the
1339
+ * caller's `inject` list must stay valid on both lines.
1340
+ */
1341
+ function resolveScope(ctx) {
1342
+ var forms = ctx.get("configForms");
1343
+ if (forms && typeof forms.get === "function") return forms.get(NS);
1344
+ var legacy = ctx.get("settingsScope");
1345
+ if (legacy && typeof legacy.bind === "function") return legacy.bind({ namespace: LEGACY_NS });
1346
+ return null;
1347
+ }
1348
+
1277
1349
  function apply(ctx) {
1278
1350
  var t = ctx.locale.bind(LOCALE);
1279
1351
  ctx.effect(function () {
1280
1352
  return ctx.locale.register(LOCALE, { zh: zh, en: en });
1281
- }, "dsh-web-icon-indicator: settings card dictionaries");
1282
- var scope = ctx.settingsScope.bind({ namespace: NS });
1283
- ctx.slots.inject("settings.plugin.item", function () {
1284
- return ctx.slots.register(
1285
- {
1286
- name: "settings.plugin.item",
1287
- key: NS,
1288
- locale: LOCALE,
1289
- inject: function () { return { scope: scope, t: t }; },
1290
- },
1291
- IconConfigCard
1292
- );
1293
- });
1353
+ }, "dsh-web-icon-indicator: settings page dictionaries");
1354
+ // The scope is resolved per render (see the inject face below), so the
1355
+ // card follows whichever provider is mounted and renders nothing while
1356
+ // neither is.
1357
+ var pageFace = function () { return { scope: resolveScope(ctx), t: t }; };
1358
+ // Register on BOTH page slots. `ctx.slots.inject(key, …)` is a no-op for a
1359
+ // slot this host never declares, so each line runs exactly the branch it
1360
+ // understands:
1361
+ // - `plugins.row.config` (≥0.1.6-alpha.2): keyed `<package>#<row id>`,
1362
+ // the row's configure control comes from this registration;
1363
+ // - `settings.plugin.item` (≤0.1.6-alpha.1): keyed by the legacy
1364
+ // settings namespace, dispatched by the settings tab per served
1365
+ // namespace.
1366
+ var start = function () {
1367
+ var disposeRow = ctx.slots.inject("plugins.row.config", function () {
1368
+ return ctx.slots.register(
1369
+ {
1370
+ name: "plugins.row.config",
1371
+ key: ROW_KEY,
1372
+ locale: LOCALE,
1373
+ inject: pageFace,
1374
+ },
1375
+ IconConfigCard
1376
+ );
1377
+ });
1378
+ var disposeItem = ctx.slots.inject("settings.plugin.item", function () {
1379
+ return ctx.slots.register(
1380
+ {
1381
+ name: "settings.plugin.item",
1382
+ key: LEGACY_NS,
1383
+ locale: LOCALE,
1384
+ inject: pageFace,
1385
+ },
1386
+ IconConfigCard
1387
+ );
1388
+ });
1389
+ return function () { disposeRow(); disposeItem(); };
1390
+ };
1391
+ // On the modern line, gate registration on the served namespace so an
1392
+ // uncomposed plugin shows no page; `whileServed` owns the returned
1393
+ // disposer, so `start` must hand one back. The legacy line needs no gate:
1394
+ // its tab only dispatches namespaces the host serves.
1395
+ var forms = ctx.get("configForms");
1396
+ if (forms && typeof forms.whileServed === "function") {
1397
+ ctx.effect(function () {
1398
+ return forms.whileServed([NS], start);
1399
+ }, "dsh-web-icon-indicator: settings page");
1400
+ } else {
1401
+ ctx.effect(start, "dsh-web-icon-indicator: settings page");
1402
+ }
1294
1403
  }
1295
1404
 
1296
1405
  exports.apply = apply;