dsh-web-icon-indicator 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-web-icon-indicator contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT or OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # dsh-web-icon-indicator
2
+
3
+ > 📖 [中文文档](README.zh.md) · [English](README.md)
4
+
5
+ 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.
6
+
7
+ ## States
8
+
9
+ | State | Visual · Icon | Animation |
10
+ | --- | --- | --- |
11
+ | `idle` | Original DeepSeek whale (default favicon) | — |
12
+ | `running` | Yellow whale | Static |
13
+ | `asking` | Yellow ⇄ red | 400 ms blink |
14
+ | `done` | Green whale | Static for 5 s, then back to idle |
15
+
16
+ The four SVG icons live in [`icons/`](./icons/) beside the package. Edit them to match your brand; the plugin re-reads them on every request, so any change is live as soon as the browser tab polls again.
17
+
18
+ ## Install
19
+
20
+ This is a standard DSH bundle plugin. Install it into the `web` profile (the GUI/TUI profiles pick it up automatically through the cordis patch layer).
21
+
22
+ From the Git source (always works, no publish needed):
23
+
24
+ ```bash
25
+ dsh plugin --profile web add github:waknow/dsh-web-icon-indicator
26
+ ```
27
+
28
+ From npm (once published):
29
+
30
+ ```bash
31
+ dsh plugin --profile web add dsh-web-icon-indicator
32
+ ```
33
+
34
+ Or from a local directory / tarball:
35
+
36
+ ```bash
37
+ dsh plugin --profile web add <path-or-tarball>
38
+ ```
39
+
40
+ Or drop the directory into `~/.dsh/profiles/web/node_modules/<name>/` and ship a `cordis.patch.yml` that matches the one shipped here.
41
+
42
+ ## Configure
43
+
44
+ All keys are optional; defaults shown.
45
+
46
+ | Key | Default | Meaning |
47
+ | --- | --- | --- |
48
+ | `iconsDir` | `<package>/icons/` | Directory of the four `*.svg` files |
49
+ | `statusPath` | `/dsh-web-icon-status.json` | JSON status endpoint |
50
+ | `iconPathPrefix` | `/dsh-web-icon-indicator` | URL prefix the four SVGs are served under |
51
+ | `askingHoldMs` | `3500` | Minimum visibility of the asking icon |
52
+ | `askingBlinkMs` | `400` | Yellow/red blink interval |
53
+ | `doneHoldMs` | `5000` | Time the done icon stays |
54
+
55
+ Override from your composition row:
56
+
57
+ ```yaml
58
+ - id: dsh-web-icon-indicator
59
+ name: 'dsh-web-icon-indicator'
60
+ config:
61
+ askingBlinkMs: 320
62
+ doneHoldMs: 4000
63
+ ```
64
+
65
+ ## How it works
66
+
67
+ - Host-only plugin: registers three routes on the existing `webServer` (status JSON, `/dsh-web-icon-indicator/*.svg`, and one `tapIndex` to inject a small browser script into every served `index.html`).
68
+ - 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.
69
+ - `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.
70
+ - The browser script polls `/dsh-web-icon-status.json` once a second and sets `<link rel="icon">`'s `href` to a `data:image/svg+xml,…` URI. Browsers don't play SVG favicon CSS animations, so the four icons are static SVGs and the `asking` blink is driven by the script swapping between yellow and red frames every `askingBlinkMs`.
71
+
72
+ ## Caveats
73
+
74
+ - Favicon SVG CSS animations do not run inside the browser's tab UI — the four shipped icons are static for that reason. Open the SVG files directly in a viewer to see the full design.
75
+ - The plugin runs in the **host** plane; it must be mounted into a profile's composition, not a session-scoped agent preset.
76
+ - File reads go through the `fs` service with the configured `iconsDir` as `cwd`. Make sure that path is readable under your deployment's sandbox policy.
77
+
78
+ ## License
79
+
80
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,80 @@
1
+ # dsh-web-icon-indicator
2
+
3
+ > 📖 [English](README.md) · [中文文档](README.zh.md)
4
+
5
+ 浏览器标签页 favicon 实时反映 DSH 会话状态——待机 / 运行中 / 提问 / 完成——让你在标签页置于后台时也能一眼看出是否有会话需要处理。
6
+
7
+ ## 状态
8
+
9
+ | 状态 | 图标 | 动画 |
10
+ | --- | --- | --- |
11
+ | `idle` 待机 | 原始 DeepSeek 鲸鱼(默认 favicon) | — |
12
+ | `running` 运行中 | 黄色鲸鱼 | 静态 |
13
+ | `asking` 提问 | 黄色 ⇄ 红色 | 400ms 闪烁 |
14
+ | `done` 完成 | 绿色鲸鱼 | 保持 5 秒后回到待机 |
15
+
16
+ 四个状态图标位于包内 `icons/` 目录。你可以随时替换它们——插件每次请求都会重新读取,浏览器下次轮询就生效。
17
+
18
+ ## 安装
19
+
20
+ 这是一个标准 DSH bundle 插件。安装到 `web` profile(GUI/TUI profile 会自动通过 cordis patch 层加载):
21
+
22
+ 从 Git 源码安装(始终可用,无需发布):
23
+
24
+ ```bash
25
+ dsh plugin --profile web add github:waknow/dsh-web-icon-indicator
26
+ ```
27
+
28
+ 从 npm 安装(发布后):
29
+
30
+ ```bash
31
+ dsh plugin --profile web add dsh-web-icon-indicator
32
+ ```
33
+
34
+ 或从本地目录 / tarball 安装:
35
+
36
+ ```bash
37
+ dsh plugin --profile web add <路径或tarball>
38
+ ```
39
+
40
+ 或将目录放进 `~/.dsh/profiles/web/node_modules/<name>/`,并附带与包内一致的 `cordis.patch.yml`。
41
+
42
+ ## 配置
43
+
44
+ 所有键均可选,默认值如下:
45
+
46
+ | 键 | 默认值 | 含义 |
47
+ | --- | --- | --- |
48
+ | `iconsDir` | `<package>/icons/` | 四个 `*.svg` 文件所在目录 |
49
+ | `statusPath` | `/dsh-web-icon-status.json` | JSON 状态端点 |
50
+ | `iconPathPrefix` | `/dsh-web-icon-indicator` | 四个 SVG 的 URL 前缀 |
51
+ | `askingHoldMs` | `3500` | 提问图标的最小保持时长 |
52
+ | `askingBlinkMs` | `400` | 黄红闪烁间隔 |
53
+ | `doneHoldMs` | `5000` | 完成图标保持时长 |
54
+
55
+ 在组合行中覆盖:
56
+
57
+ ```yaml
58
+ - id: dsh-web-icon-indicator
59
+ name: 'dsh-web-icon-indicator'
60
+ config:
61
+ askingBlinkMs: 320
62
+ doneHoldMs: 4000
63
+ ```
64
+
65
+ ## 实现原理
66
+
67
+ - Host-only 插件:在现有 `webServer` 上注册三个路由(状态 JSON、`/dsh-web-icon-indicator/*.svg`,以及一个 `tapIndex` 向每个 `index.html` 注入小段浏览器脚本)。
68
+ - 状态按 `agents.list()` 聚合,优先级 `asking > running > done > idle`。每次请求都会执行一次 `reconcile()` 检测 running → idle 的转换,因为 `agent/status` 的 idle 事件在回合结束时并不保证送达。
69
+ - `ask_user_question` 工具调用(通过 `tools/pre-execute` / `tools/result`)把会话置为 `asking`,带可配置的最小保持时长,即使你立刻回答,图标也会保持可见。
70
+ - 浏览器脚本每秒轮询 `/dsh-web-icon-status.json`,把 `<link rel="icon">` 的 `href` 设为 `data:image/svg+xml,…` URI。浏览器不会播放 SVG favicon 的 CSS 动画,所以四个图标是静态 SVG,`asking` 的闪烁由脚本每 `askingBlinkMs` 交换黄红两帧实现。
71
+
72
+ ## 已知限制
73
+
74
+ - favicon 的 SVG CSS 动画在浏览器标签页 UI 中不会运行——四个内置图标因此是静态的。直接用浏览器打开 SVG 文件可以看到完整设计。
75
+ - 插件运行在 **host** 平面,必须挂载进 profile 的组合配置,不能作为会话级 agent preset。
76
+ - 文件读取走 `fs` 服务,以配置的 `iconsDir` 为 `cwd`。请确保该路径在部署环境的沙箱策略下可读。
77
+
78
+ ## 许可
79
+
80
+ MIT
@@ -0,0 +1,8 @@
1
+ # dsh-web-icon-indicator bundle patch: inserts the host-only plugin row into a
2
+ # profile's layer stack. The plugin reads its icon assets from `icons/`
3
+ # beside this file (relative to the package directory).
4
+ #
5
+ # Install: `dsh plugin --profile web add <path-or-git-url>`
6
+ - insert:
7
+ - id: dsh-web-icon-indicator
8
+ name: 'dsh-web-icon-indicator'
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
2
+ <style>
3
+ #path { fill: #E5484D; }
4
+ </style>
5
+ <path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
6
+ </svg>
package/icons/done.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
2
+ <style>
3
+ #path { fill: #22A06B; }
4
+ </style>
5
+ <path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
6
+ </svg>
package/icons/idle.svg ADDED
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
2
+ <style>
3
+ @media (prefers-color-scheme: dark) {
4
+ path { fill: #fff; }
5
+ }
6
+ </style>
7
+ <path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
8
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
2
+ <style>
3
+ #path { fill: #FACC15; }
4
+ </style>
5
+ <path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
6
+ </svg>
package/lib/index.js ADDED
@@ -0,0 +1,360 @@
1
+ /**
2
+ * * dsh-web-icon-indicator — Host-only plugin that mirrors the
3
+ * current DSH session state onto the browser tab favicon. Four states:
4
+ * `idle` (default favicon), `running` (static yellow whale),
5
+ * `asking` (yellow/red blinking), `done` (static green whale, 5s).
6
+ *
7
+ * The plugin is self-contained: it reads four SVG icons from its
8
+ * `icons/` directory (relative to the package) and serves them through a
9
+ * static `/dsh-web-icon-indicator/<name>.svg` route on the existing webServer,
10
+ * while injecting a small browser script that polls `/dsh-web-icon-status.json`
11
+ * and applies the right icon to `<link rel="icon">`.
12
+ *
13
+ * State derivation:
14
+ * - `agent/status` driving events update a per-session map.
15
+ * - `tools/pre-execute` on `ask_user_question` flips the session into
16
+ * `asking`; a 3.5 s minimum hold keeps the asking icon visible even
17
+ * when the user answers immediately. `tools/result` lets the hold
18
+ * expire cleanly, restoring the actual agent status.
19
+ * - `agent/turn-stopping` flips `running`/`asking` into `done`.
20
+ * - `reconcile()` (called on every status request) watches
21
+ * `agents.list()` for running→idle transitions as a fallback, since
22
+ * `agent/status`'s idle event is not reliably delivered at turn end.
23
+ *
24
+ * No Client half: the browser script runs outside the sandbox by being
25
+ * injected into the served index.html, where `document` is freely available.
26
+ * The Cordis Client sandbox is restricted to `ctx` / `React` / `host` /
27
+ * `styles` / `console` and intentionally cannot touch the favicon.
28
+ *
29
+ * Configuration object (all optional):
30
+ * iconsDir Absolute directory holding idle.svg / running.svg /
31
+ * asking.svg / done.svg. Defaults to `<package>/icons/`.
32
+ * statusPath JSON status endpoint path. Default: `/dsh-web-icon-status.json`.
33
+ * iconPathPrefix URL prefix for static icon files. Default: `/dsh-web-icon-indicator`.
34
+ * askingHoldMs Minimum visibility for the asking icon (ms). Default 3500.
35
+ * askingBlinkMs Yellow/red switch interval for asking (ms). Default 400.
36
+ * doneHoldMs How long the done icon stays before falling back to idle.
37
+ * Default 5000.
38
+ *
39
+ * @module dsh-web-icon-indicator
40
+ */
41
+ import { dirname, join } from "node:path";
42
+ import { fileURLToPath } from "node:url";
43
+
44
+ const __dirname = dirname(fileURLToPath(import.meta.url));
45
+
46
+ const DEFAULTS = Object.freeze({
47
+ askingHoldMs: 3500,
48
+ askingBlinkMs: 400,
49
+ doneHoldMs: 5000,
50
+ statusPath: "/dsh-web-icon-status.json",
51
+ iconPathPrefix: "/dsh-web-icon-indicator",
52
+ iconsDir: join(__dirname, "..", "icons"),
53
+ });
54
+
55
+ /** Browser script injected into every served index.html response. */
56
+ const INJECTED_SCRIPT = `
57
+ (function () {
58
+ try {
59
+ if (window.__DSH_WEB_ICON_INDICATOR__) return;
60
+ window.__DSH_WEB_ICON_INDICATOR__ = true;
61
+ var ORIGINAL = null;
62
+ var TIMER = null;
63
+ var ANIM = null;
64
+ var ANIM_STATE = null;
65
+ var ICONS = {};
66
+ var LAST_STATE = null;
67
+ var STATUS_PATH = "__STATUS_PATH__";
68
+ var ICON_PREFIX = "__ICON_PREFIX__";
69
+ var ASKING_BLINK_MS = __ASKING_BLINK_MS__;
70
+
71
+ function captureOriginal() {
72
+ var link = document.querySelector("link[rel='icon']");
73
+ if (link && ORIGINAL === null) ORIGINAL = { href: link.getAttribute("href") || "", type: link.getAttribute("type") || "" };
74
+ }
75
+ function restore() {
76
+ var link = document.querySelector("link[rel='icon']");
77
+ if (link && ORIGINAL) { link.setAttribute("href", ORIGINAL.href); if (ORIGINAL.type) link.setAttribute("type", ORIGINAL.type); }
78
+ }
79
+ function setHref(uri) {
80
+ var link = document.querySelector("link[rel='icon']");
81
+ if (!link) return;
82
+ try { link.setAttribute("href", uri); link.setAttribute("type", "image/svg+xml"); } catch (e) {}
83
+ }
84
+ function iconUri(name) {
85
+ if (ICONS[name]) return Promise.resolve(ICONS[name]);
86
+ return fetch(location.origin + ICON_PREFIX + "/" + name + ".svg?t=" + Date.now(), { cache: "no-store" })
87
+ .then(function (r) { if (!r.ok) throw new Error("icon " + r.status); return r.text(); })
88
+ .then(function (txt) { ICONS[name] = "data:image/svg+xml," + encodeURIComponent(txt); return ICONS[name]; })
89
+ .catch(function () { return null; });
90
+ }
91
+ function stopAnim() { if (ANIM) { clearInterval(ANIM); ANIM = null; } ANIM_STATE = null; }
92
+ function apply(state) {
93
+ var link = document.querySelector("link[rel='icon']");
94
+ if (!link) return;
95
+ LAST_STATE = state;
96
+ if (state === "idle" || state == null) { stopAnim(); restore(); return; }
97
+ if (state === "asking") {
98
+ if (ANIM_STATE === "asking") return;
99
+ stopAnim();
100
+ ANIM_STATE = "asking";
101
+ Promise.all([iconUri("running"), iconUri("asking")]).then(function (uris) {
102
+ if (!uris[0] || !uris[1] || !link.isConnected) return;
103
+ var i = 0;
104
+ setHref(uris[i]);
105
+ ANIM = setInterval(function () { i = 1 - i; setHref(uris[i]); }, ASKING_BLINK_MS);
106
+ });
107
+ return;
108
+ }
109
+ stopAnim();
110
+ iconUri(state).then(function (uri) { if (uri && link.isConnected) setHref(uri); });
111
+ }
112
+ function poll() {
113
+ fetch(location.origin + STATUS_PATH, { cache: "no-store" })
114
+ .then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); })
115
+ .then(function (j) { apply(j.state || "idle"); })
116
+ .catch(function () { stopAnim(); restore(); if (TIMER) { clearInterval(TIMER); TIMER = null; } });
117
+ }
118
+ captureOriginal();
119
+ TIMER = setInterval(poll, 1000);
120
+ poll();
121
+ window.addEventListener("beforeunload", function () { stopAnim(); if (TIMER) clearInterval(TIMER); restore(); });
122
+ } catch ( e) {}
123
+ })();
124
+ `;
125
+
126
+ /**
127
+ * Resolve the icon directory. Accepts an explicit absolute path from
128
+ * config, falls back to the bundled `icons/` directory beside the
129
+ * package's compiled `lib/index.js`. Rejects relative paths that escape
130
+ * the package; the bundled default is package-relative and always safe.
131
+ */
132
+ function resolveIconsDir(configured) {
133
+ if (configured) return configured;
134
+ return join(__dirname, "..", "icons");
135
+ }
136
+
137
+ /**
138
+ * Cordis plugin entry. Returns the standard `{ apply, inject, config }`
139
+ * shape so the host composition can mount it once at startup.
140
+ */
141
+ export default {
142
+ name: "dsh-web-icon-indicator",
143
+ inject: ["webServer", "timer", "agents", "fs", "sandboxPolicy"],
144
+ config: {
145
+ askingHoldMs: DEFAULTS.askingHoldMs,
146
+ askingBlinkMs: DEFAULTS.askingBlinkMs,
147
+ doneHoldMs: DEFAULTS.doneHoldMs,
148
+ statusPath: DEFAULTS.statusPath,
149
+ iconPathPrefix: DEFAULTS.iconPathPrefix,
150
+ iconsDir: undefined,
151
+ },
152
+ apply(ctx) {
153
+ const cfg = { ...DEFAULTS, ...(ctx.get("config") || {}) };
154
+ const webServer = ctx.webServer;
155
+ const agents = ctx.agents;
156
+ const fs = ctx.fs;
157
+ const sp = ctx.sandboxPolicy;
158
+ const iconsDir = resolveIconsDir(cfg.iconsDir);
159
+ const lastSeen = new Map();
160
+
161
+ // -- State machine -------------------------------------------------------
162
+ const states = new Map(); // agentId -> { state, since }
163
+ const asking = new Set(); // agentIds whose icon is currently pinned to asking
164
+ const askDone = new Set(); // agentIds whose ask_user_question tool call already returned
165
+ const askTimers = new Map(); // agentId -> ctx.timer.timeout disposer
166
+
167
+ const setState = (id, state) => {
168
+ const prev = states.get(id);
169
+ const now = Date.now();
170
+ if (state === "asking") asking.add(id);
171
+ if (prev && prev.state === state) { states.set(id, { state, since: prev.since }); return; }
172
+ states.set(id, { state, since: now });
173
+ if (state === "done") {
174
+ const handle = ctx.timer.timeout(() => {
175
+ const cur = states.get(id);
176
+ if (cur && cur.state === "done") states.set(id, { state: "idle", since: Date.now() });
177
+ }, cfg.doneHoldMs);
178
+ ctx.effect(() => handle); // register for cleanup
179
+ }
180
+ };
181
+
182
+ ctx.on("agent/status", (payload) => {
183
+ const id = payload.agent?.id ?? null;
184
+ if (id == null) return;
185
+ if (payload.status === "running") setState(id, asking.has(id) ? "asking" : "running");
186
+ else if (payload.status === "idle") {
187
+ const prev = states.get(id);
188
+ if (asking.has(id)) setState(id, "asking");
189
+ else if (prev?.state === "running") setState(id, "done");
190
+ else setState(id, "idle");
191
+ }
192
+ });
193
+
194
+ ctx.on("agent/turn-stopping", (payload) => {
195
+ const id = payload.agent?.id ?? null;
196
+ if (id == null) return;
197
+ const cur = states.get(id);
198
+ if (!cur || (cur.state !== "asking" && cur.state !== "running")) return;
199
+ if (!asking.has(id)) setState(id, "done");
200
+ });
201
+
202
+ // Asking: visible minimum (askingHoldMs). Re-arm timer while user
203
+ // is still answering; clean up to live agent status once returned.
204
+ const scheduleAskCheck = (id) => {
205
+ const prevHandle = askTimers.get(id);
206
+ if (prevHandle) { try { prevHandle(); } catch (e) {} }
207
+ const handle = ctx.timer.timeout(() => {
208
+ askTimers.delete(id);
209
+ if (!asking.has(id)) return;
210
+ if (!askDone.has(id)) { scheduleAskCheck(id); return; }
211
+ asking.delete(id);
212
+ askDone.delete(id);
213
+ const live = agents.get(id);
214
+ if (live) setState(id, live.status === "running" ? "running" : "idle");
215
+ else setState(id, "idle");
216
+ }, cfg.askingHoldMs);
217
+ askTimers.set(id, handle);
218
+ };
219
+
220
+ ctx.on("tools/pre-execute", (exec, next) => {
221
+ if (exec.name === "ask_user_question") {
222
+ const id = exec.agent?.id ?? null;
223
+ if (id != null) {
224
+ asking.add(id);
225
+ askDone.delete(id);
226
+ setState(id, "asking");
227
+ scheduleAskCheck(id);
228
+ }
229
+ }
230
+ return typeof next === "function" ? next() : undefined;
231
+ });
232
+
233
+ ctx.on("tools/result", (exec, _result) => {
234
+ if (exec.name === "ask_user_question") {
235
+ const id = exec.agent?.id ?? null;
236
+ if (id != null) askDone.add(id);
237
+ }
238
+ });
239
+
240
+ ctx.on("agent/disposed", (payload) => {
241
+ const id = payload.agent?.id ?? null;
242
+ if (id == null) return;
243
+ states.delete(id);
244
+ asking.delete(id);
245
+ askDone.delete(id);
246
+ lastSeen.delete(id);
247
+ const t = askTimers.get(id);
248
+ if (t) { try { t(); } catch (e) {} askTimers.delete(id); }
249
+ });
250
+
251
+ // Reconcile running->idle transitions on every status request. The
252
+ // `agent/status` event's idle delivery is not guaranteed, so polling
253
+ // agents.list() is the authoritative fallback.
254
+ const reconcile = () => {
255
+ for (const a of agents.list()) {
256
+ const id = a.id;
257
+ const st = a.status;
258
+ const prev = lastSeen.get(id);
259
+ lastSeen.set(id, { status: st, at: Date.now() });
260
+ if (asking.has(id)) continue;
261
+ if (prev?.status === "running" && st === "idle") {
262
+ setState(id, "done");
263
+ } else if (st === "idle" && !states.has(id)) {
264
+ setState(id, "idle");
265
+ }
266
+ }
267
+ const liveIds = new Set(agents.list().map((x) => x.id));
268
+ for (const id of Array.from(lastSeen.keys())) {
269
+ if (!liveIds.has(id)) lastSeen.delete(id);
270
+ }
271
+ };
272
+
273
+ const agentStateOf = (id) => {
274
+ const live = agents.get(id);
275
+ if (!live) return null;
276
+ if (asking.has(id)) return "asking";
277
+ return live.status === "running" ? "running" : "idle";
278
+ };
279
+
280
+ const aggregate = () => {
281
+ reconcile();
282
+ const order = ["asking", "running", "done", "idle"];
283
+ let best = null;
284
+ for (const a of agents.list()) {
285
+ const id = a.id;
286
+ const s = agentStateOf(id);
287
+ const rank = order.indexOf(s);
288
+ if (rank === -1) continue;
289
+ if (best === null || rank < best.rank) best = { rank, id, state: s };
290
+ }
291
+ for (const [id, rec] of states) {
292
+ if (rec.state === "done") {
293
+ const rank = order.indexOf("done");
294
+ if (best === null || rank < best.rank) best = { rank, id, state: "done" };
295
+ }
296
+ }
297
+ return best ? { state: best.state, since: (states.get(best.id) || {}).since || Date.now() } : { state: "idle", since: Date.now() };
298
+ };
299
+
300
+ // -- Routes --------------------------------------------------------------
301
+ ctx.effect(() => webServer.register({
302
+ kind: "exact",
303
+ path: cfg.statusPath,
304
+ handler: (_req, res) => {
305
+ res.statusCode = 200;
306
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
307
+ res.setHeader("Cache-Control", "no-store");
308
+ res.end(JSON.stringify(aggregate()));
309
+ },
310
+ }));
311
+
312
+ ctx.effect(() => webServer.register({
313
+ kind: "prefix",
314
+ path: cfg.iconPathPrefix,
315
+ handler: async (req, res) => {
316
+ let pathname = "/";
317
+ try { pathname = new URL(String(req?.url ?? "/"), "http://x").pathname; } catch (e) {}
318
+ const name = pathname.replace(/^\/[^/]+\//, "");
319
+ if (!/^(idle|running|asking|done)\.svg$/.test(name)) {
320
+ res.statusCode = 404;
321
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
322
+ res.end("bad name: " + name);
323
+ return;
324
+ }
325
+ if (!fs) {
326
+ res.statusCode = 503;
327
+ res.end("fs service unavailable");
328
+ return;
329
+ }
330
+ let text = null;
331
+ try {
332
+ const target = await fs.resolve(name, { cwd: iconsDir });
333
+ text = await fs.readText(target);
334
+ } catch (e) {
335
+ res.statusCode = 404;
336
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
337
+ res.end("not found: " + name);
338
+ return;
339
+ }
340
+ res.statusCode = 200;
341
+ res.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
342
+ res.setHeader("Cache-Control", "no-store");
343
+ res.end(text);
344
+ },
345
+ }));
346
+
347
+ // -- Script injection ----------------------------------------------------
348
+ const script = INJECTED_SCRIPT
349
+ .replace("__STATUS_PATH__", cfg.statusPath)
350
+ .replace("__ICON_PREFIX__", cfg.iconPathPrefix)
351
+ .replace("__ASKING_BLINK_MS__", String(cfg.askingBlinkMs));
352
+
353
+ ctx.effect(() => webServer.tapIndex((html) => {
354
+ if (html.indexOf("window.__DSH_WEB_ICON_INDICATOR__") !== -1) return html;
355
+ const tag = `<script id="dsh-web-icon-indicator">${script}<\/script>`;
356
+ if (/<\/body>/.test(html)) return html.replace(/<\/body>/, tag + "</body>");
357
+ return html + tag;
358
+ }));
359
+ },
360
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Public configuration surface for the dsh-web-icon-indicator host plugin.
3
+ *
4
+ * All keys are optional; the plugin falls back to sane defaults baked into
5
+ * `lib/index.js`. Pass this object from your `cordis.yml` row:
6
+ *
7
+ * ```yaml
8
+ * - id: dsh-web-icon-indicator
9
+ * name: 'dsh-web-icon-indicator'
10
+ * config:
11
+ * askingBlinkMs: 320
12
+ * doneHoldMs: 4000
13
+ * iconsDir: /opt/dsh/icon-indicator-icons
14
+ * ```
15
+ */
16
+ export interface DshWebIconIndicatorConfig {
17
+ /**
18
+ * Absolute path to the directory holding `idle.svg`, `running.svg`,
19
+ * `asking.svg`, and `done.svg`. Defaults to `<package>/icons/`.
20
+ */
21
+ iconsDir?: string;
22
+ /** Status JSON endpoint the browser polls. Default `/dsh-web-icon-status.json`. */
23
+ statusPath?: string;
24
+ /** URL prefix where the four SVGs are served. Default `/dsh-web-icon-indicator`. */
25
+ iconPathPrefix?: string;
26
+ /** Minimum visibility of the asking icon in milliseconds. Default 3500. */
27
+ askingHoldMs?: number;
28
+ /** Yellow/red switch interval for the asking blink in milliseconds. Default 400. */
29
+ askingBlinkMs?: number;
30
+ /** Time the done icon stays before falling back to idle, in milliseconds. Default 5000. */
31
+ doneHoldMs?: number;
32
+ }
33
+
34
+ export interface DshWebIconIndicatorAggregate {
35
+ /** Highest-priority state across all live sessions. */
36
+ state: "idle" | "running" | "asking" | "done";
37
+ /** Epoch millis when that aggregate state was first entered. */
38
+ since: number;
39
+ }
40
+
41
+ export default function apply(ctx: unknown): void;
42
+ export {};
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "dsh-web-icon-indicator",
3
+ "description": "Browser tab favicon reflects the DSH session state: idle / running / asking / done, with state-driven SVG icons read from this plugin's `icons/` directory. · 浏览器标签页 favicon 实时反映 DSH 会话状态:待机 / 运行中 / 提问 / 完成,图标从本插件的 `icons/` 目录加载。",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/waknow/dsh-web-icon-indicator.git"
12
+ },
13
+ "homepage": "https://github.com/waknow/dsh-web-icon-indicator",
14
+ "bugs": {
15
+ "url": "https://github.com/waknow/dsh-web-icon-indicator/issues"
16
+ },
17
+ "dsh": {
18
+ "bundle": {
19
+ "patch": "./cordis.patch.yml"
20
+ }
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./lib/types/index.d.ts",
25
+ "default": "./lib/index.js"
26
+ },
27
+ "./cordis.patch.yml": "./cordis.patch.yml",
28
+ "./package.json": "./package.json",
29
+ "./icons": "./icons/*.svg"
30
+ },
31
+ "files": [
32
+ "lib",
33
+ "icons",
34
+ "cordis.patch.yml",
35
+ "README.md",
36
+ "README.zh.md",
37
+ "LICENSE"
38
+ ],
39
+ "keywords": [
40
+ "deepseek",
41
+ "harness",
42
+ "dsh",
43
+ "dsh-plugin",
44
+ "favicon",
45
+ "icon-indicator",
46
+ "browser-tab"
47
+ ]
48
+ }