dsh-notify-me 1.1.8 → 1.2.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 +32 -0
- package/README.en.md +20 -6
- package/README.md +20 -6
- package/lib/client.js +119 -52
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.1] — 2026-10-03
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **提示音叠着响两声**:`new Notification(...)` 没带 `silent: true`,平台自带通知音与插件自己的 WebAudio 提示音各响一遍。现在通知一律静音,声音统一由插件承担(attention / done 两种音型、音量设置不变);设置页「提示音」关掉即完全安静。
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
- `smoke/cordis-host-test.mjs` 借用 cordis 的探测路径补上 `~/.dsh/profiles/node_modules`(`healProfilesModuleFallback` 维护的平铺回退目录):此前只探测 `profiles/web/node_modules`,多数机器上最强的那套真 cordis 端到端用例会静默跳过。
|
|
12
|
+
|
|
13
|
+
## [1.2.0] — 2026-09-30
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
- **`0.1.6-alpha.2` 及以后(含 `0.2.0-rc.2`)宿主上「模型需要你操作」整条通道再次静默失效**:上游把待办交互从 `uiSession.pendingInteractions`(`Map<sessionId, interaction>`)搬进了 `uiSession.sessionStatus`(`Map<sessionId, {running, pendingInteraction, completionUnread}>`),旧 store 在新宿主上根本不存在——插件读不到就静默不提醒(审批卡片出现、模型停下,系统通知/提示音/标题标记全无;而「回复完成」与设置页测试按钮照常,现象与 1.1.5 那次一模一样)。现按宿主代际自动选源:`≥ 0.1.6-alpha.2` 绑定 `sessionStatus`,`0.1.2 .. 0.1.5` 仍用 `pendingInteractions`,`≤ 0.1.1` 仍走控制器快照;`window.__dshNotifyMe.debug()` 新增 `uiSessionSource`,直接显示这条通道绑的是哪个来源。
|
|
17
|
+
- **`0.1.6` 及以后的宿主上认不出「当前会话」**:会话列表快照不再有 `current`(`SessionListState` 只剩 `ids / byId / phase / projectionsBySession`),于是当前会话面(face)订阅从未建立、`onListChanged()` 还把当前会话当成后台会话评估——后果是 1.1.7 的「当前对话不弹通知」规则失效(审批卡片就在屏幕上,Toast 仍会盖上去),标签页标记的归属也随之错位。现在按宿主自己的口径推导:有 `current` 用它,没有就取 `retainedBy.mainView > 0` 的那一行(与内置 `DocumentTitle` / `ui-workspace` / `ui-session` 的判定一致)。
|
|
18
|
+
- **`0.1.6` 及以后点通知切不回对应会话**:`sessions.open(id)` 已被移除,导航改由 `uiWorkspace.openSession(id)` 承担;插件现在两个都试(`open` 优先,旧宿主行为不变),会话已不在列表时依旧不调用。`debug()` 新增 `currentSession` 便于核对。
|
|
19
|
+
|
|
20
|
+
### 新增
|
|
21
|
+
- `sessionStatus` 适配完整:同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
|
|
22
|
+
|
|
23
|
+
### 变更
|
|
24
|
+
- `peerDependencies` 放宽为 `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || >=0.1.6-alpha.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`:原范围在 `0.2.0-rc.2` 上被 `evaluatePluginCompatibility` 判为不兼容,`dsh plugin add` 会拒绝并回滚安装(`0.3` 线仍被拒)。`dsh.compatibility.dshReleases` 增加 `0.2.0-rc.2: compatible`。
|
|
25
|
+
- 版本号 1.1.8 → 1.2.0(新宿主代际);`window.__dshNotifyMe.version` 同步。
|
|
26
|
+
- 设置页字典注册改为把 `locale.register()` 返回的 disposer 交给 `ctx.effect`(与官方 `ui-approval` 写法一致),客户端插件 HMR 重载时不会因「同名 namespace 已注册」而丢掉设置页。
|
|
27
|
+
- `uiSession` 绑定的诊断降噪:真实 `0.2.0-rc.2` 页面上每次加载都会出现的那条 `uiSession is provided but not reachable` 警告其实是**启动竞态**(本插件的客户端条目常早于 `ui-session` 条目激活,此时 `ctx.get()` 按 cordis 的严格语义拿不到尚未激活的服务),并非缺陷。现在只有**能拿到服务实例、但它既没有 `sessionStatus` 也没有 `pendingInteractions`** 时才打一条警告(并提示附上 `debug()` 输出);「还没可见」保持安静,由 1.5s 重试定时器接管,`debug().uiSessionNote` 仍可查。
|
|
28
|
+
|
|
29
|
+
### 测试
|
|
30
|
+
- `smoke/smoke-test.cjs` 增加第三代宿主用例(`uiSession.sessionStatus`、列表无 `current`、无 `sessions.open`):审批/提问/方案确认文案与详情、去重、等待消失释放标记、挂载前已存在的等待提醒一次、当前对话静默 + 转后台补发、后台会话前台提醒、主开关、`retainedBy.mainView` 推导当前会话、点通知走 `uiWorkspace.openSession`、会话已消失不打开。
|
|
31
|
+
- `smoke/cordis-host-test.mjs` 增加真 cordis 的 `sessionStatus` 宿主(兄弟插件提供 `sessionStatus` 与 `uiWorkspace`,`sessions` 不带 `open`):断言订阅绑到 `sessionStatus`、审批只提醒一次且正文带工具名 + 理由、标题标记设置与释放、点通知调用 `uiWorkspace.openSession`。原有 `pendingInteractions` 宿主与无 `uiSession` 的旧宿主用例保留。
|
|
32
|
+
- 自检命令补 `DSH_CORDIS_PATH`:源码 checkout 里没有 profile 依赖时,用它把 `cordis-host-test.mjs` 指向 checkout 内的 `vendor/cordis/lib/index.js`(否则该用例按设计跳过)。
|
|
33
|
+
|
|
34
|
+
### 已知限制
|
|
35
|
+
- `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文不再附带回复摘要:宿主快照不再提供 `nodes`,摘要取不到;提醒本身照常触发。
|
|
36
|
+
|
|
5
37
|
## [1.1.8] — 2026-09-24
|
|
6
38
|
|
|
7
39
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -74,7 +74,15 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
74
74
|
|
|
75
75
|
Restart `dsh web`, then hard-refresh the page (Ctrl+Shift+R).
|
|
76
76
|
|
|
77
|
-
**Compatibility** —
|
|
77
|
+
**Compatibility** — the plugin picks its interaction source per host generation; per-release declarations live in `package.json` under `dsh.compatibility.dshReleases`:
|
|
78
|
+
|
|
79
|
+
| Host generation | "Needs you" interaction source | Current session | Toast-click navigation |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| `<= 0.1.1-rc.2` | controller snapshot `pending[]` | snapshot `current` | `sessions.open()` |
|
|
82
|
+
| `0.1.2-alpha.2 .. 0.1.5-rc.3` | `uiSession.pendingInteractions` | snapshot `current` | `sessions.open()` |
|
|
83
|
+
| `>= 0.1.6-alpha.2` (incl. `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
84
|
+
|
|
85
|
+
Verified to actually activate on `0.1.2-rc.1`, `0.1.5-rc.2` and `0.2.0-rc.2` (each booted in its own profile: the Settings → Notifications section registers, i.e. the plugin's `apply` really runs), and previously on `0.1.1-rc.2`. Two traps are worth remembering: a package listed in `dsh.client.inject` that a newer runtime no longer ships parks the client entry at `pending (waiting for services: …)` forever — which is what broke 1.1.3 on `0.1.2-rc.1` and later; and when the host renames a store or a field nothing throws, alerts just go **silent** — `0.2.0-rc.2` moved `pendingInteractions`→`sessionStatus`, `list.current`→`retainedBy.mainView` and `sessions.open()`→`uiWorkspace.openSession()`. So every adaptation must confirm `window.__dshNotifyMe.debug()` reports `bound` and which source answered.
|
|
78
86
|
|
|
79
87
|
**Verify** — open the DevTools console and run:
|
|
80
88
|
|
|
@@ -110,10 +118,11 @@ window.__dshNotifyMe.resetConfig() // restore defaults
|
|
|
110
118
|
|
|
111
119
|
## How it works
|
|
112
120
|
|
|
113
|
-
The
|
|
121
|
+
The reminder core picks its interaction source per host generation (see the table above); the current session always comes from the client `sessions` service:
|
|
114
122
|
|
|
115
|
-
- the **selected session's** `
|
|
116
|
-
- **
|
|
123
|
+
- the **selected session's** `SessionSnapshot`: a `running: true → false` edge means a reply finished;
|
|
124
|
+
- **pending interactions**: a new key in `uiSession.sessionStatus` (`>= 0.1.6`, per-session `pendingInteraction`) or in `pendingInteractions` (`0.1.2 .. 0.1.5`) means the agent is waiting on you — approval requests carry the tool name and reason, questions their text; older hosts fall back to the controller snapshot's `pending[]`;
|
|
125
|
+
- **every other listed session's** summary: a `running: true → false` edge (or the legacy `completed` flag) alerts you about background work.
|
|
117
126
|
|
|
118
127
|
The reminder core stays dependency-free and self-contained: notification copy resolves at alert time from the chosen language (follow-interface / Simplified Chinese / English). The Settings page is an **optional** React surface — it only registers into Settings when the web profile provides the `slots` / `locale` services and `react`; without them the plugin degrades gracefully to alerts-only (no Settings page).
|
|
119
128
|
|
|
@@ -124,16 +133,21 @@ The reminder core stays dependency-free and self-contained: notification copy re
|
|
|
124
133
|
- The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
|
|
125
134
|
- Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
|
|
126
135
|
- Config lives in browser `localStorage`: switching browsers/devices or clearing site data returns to defaults (one-click restore in the Settings page).
|
|
136
|
+
- On `>= 0.1.6` (including `0.2.0-rc.2`) the "reply finished" toast body carries only the session label, no assistant snippet: host snapshots no longer expose `nodes`. The alert itself still fires.
|
|
137
|
+
- The title marker is written into `document.title` alongside the host: when the host recomputes the title it can overwrite the marker until the next alert event rewrites it.
|
|
127
138
|
|
|
128
139
|
## Development
|
|
129
140
|
|
|
130
141
|
```powershell
|
|
131
142
|
node --check lib\client.js
|
|
132
143
|
node --check lib\index.js
|
|
133
|
-
node smoke\smoke-test.cjs
|
|
134
|
-
|
|
144
|
+
node smoke\smoke-test.cjs # offline state-machine test: three host generations + master switch + zh/en cases
|
|
145
|
+
node smoke\cordis-host-test.mjs # real-cordis end-to-end (skips when no local DSH installation is found)
|
|
146
|
+
npm pack --dry-run # preview the published tarball
|
|
135
147
|
```
|
|
136
148
|
|
|
149
|
+
`cordis-host-test.mjs` borrows `@deepseek-ai/cordis` from a local DSH installation; in a source checkout with no profile dependencies, point it there with `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js`.
|
|
150
|
+
|
|
137
151
|
## License
|
|
138
152
|
|
|
139
153
|
MIT — see [LICENSE](LICENSE).
|
package/README.md
CHANGED
|
@@ -74,7 +74,15 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
74
74
|
|
|
75
75
|
重启 `dsh web` 后硬刷新页面(Ctrl+Shift+R)。
|
|
76
76
|
|
|
77
|
-
**兼容性** —
|
|
77
|
+
**兼容性** — 插件按宿主代际自动选源,逐版本声明见 `package.json` 的 `dsh.compatibility.dshReleases`:
|
|
78
|
+
|
|
79
|
+
| 宿主代际 | 「需要你」交互来源 | 判断当前会话 | 点通知切会话 |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| `≤ 0.1.1-rc.2` | 控制器快照的 `pending[]` | 快照 `current` | `sessions.open()` |
|
|
82
|
+
| `0.1.2-alpha.2 .. 0.1.5-rc.3` | `uiSession.pendingInteractions` | 快照 `current` | `sessions.open()` |
|
|
83
|
+
| `≥ 0.1.6-alpha.2`(含 `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
84
|
+
|
|
85
|
+
实测**真正激活**(各自独立 profile 启动:设置里出现「通知提醒」分区,即插件的 `apply` 确实执行)于 `0.1.2-rc.1`、`0.1.5-rc.2` 与 `0.2.0-rc.2`;更早的 `0.1.1-rc.2` 亦验证过。两个坑值得记住:一是 `dsh.client.inject` 里列了新版运行时已不再提供的包,客户端条目会停在 `pending (waiting for services: …)` 而永不执行——1.1.3 在 `0.1.2-rc.1` 及以后正是这样失效的;二是宿主换存储/字段名时**不会报错**,提醒只会静默失效——`0.2.0-rc.2` 上 `pendingInteractions`→`sessionStatus`、`list.current`→`retainedBy.mainView`、`sessions.open()`→`uiWorkspace.openSession()` 都是这一类,所以每次适配都要用 `window.__dshNotifyMe.debug()` 确认真的是 `bound`、以及用的是哪个来源。
|
|
78
86
|
|
|
79
87
|
**验证** — F12 控制台执行:
|
|
80
88
|
|
|
@@ -106,10 +114,11 @@ window.__dshNotifyMe.resetConfig() // 恢复默认
|
|
|
106
114
|
|
|
107
115
|
## 工作原理
|
|
108
116
|
|
|
109
|
-
|
|
117
|
+
提醒核心按宿主代际取源(见上表),当前会话一律来自客户端 `sessions` 服务:
|
|
110
118
|
|
|
111
|
-
- **当前会话** `
|
|
112
|
-
-
|
|
119
|
+
- **当前会话** `SessionSnapshot`:`running` true→false = 回复完成;
|
|
120
|
+
- **待办交互**:`uiSession` 的 `sessionStatus`(`≥ 0.1.6`,逐会话 `pendingInteraction`)或 `pendingInteractions`(`0.1.2 .. 0.1.5`)出现新 key = 模型在等你,容器里带上审批工具名 / 理由、提问文本;旧宿主回落到控制器快照的 `pending[]`;
|
|
121
|
+
- **其它已列会话**摘要:`running` true→false(或旧宿主的 `completed` 边沿)= 后台工作提醒。
|
|
113
122
|
|
|
114
123
|
提醒核心仍然零第三方运行时依赖、完全自包含:通知文字按所选语言(跟随界面 / 中文 / English)即时解析。设置页是**可选**的 React 呈现层——当 DSH web profile 提供 `slots` / `locale` / `react` 时才注册进「设置 → 通知提醒」,缺任一能力时插件自动降级为纯提醒(无设置页),不影响功能。
|
|
115
124
|
|
|
@@ -120,16 +129,21 @@ window.__dshNotifyMe.resetConfig() // 恢复默认
|
|
|
120
129
|
- 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
|
|
121
130
|
- 未授权通知权限时只有提示音与标题标记。
|
|
122
131
|
- 配置存在浏览器 `localStorage`:换浏览器/设备或清除站点数据后会回到默认值(设置页可一键恢复默认)。
|
|
132
|
+
- `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文只有会话名,不再附带回复摘要——宿主快照已不再提供 `nodes`;提醒本身照常触发。
|
|
133
|
+
- 标题标记由宿主与插件共同写 `document.title`:宿主重算标题时标记可能被覆盖,直到下一次提醒事件重新写成。
|
|
123
134
|
|
|
124
135
|
## 开发自检
|
|
125
136
|
|
|
126
137
|
```powershell
|
|
127
138
|
node --check lib\client.js
|
|
128
139
|
node --check lib\index.js
|
|
129
|
-
node smoke\smoke-test.cjs
|
|
130
|
-
|
|
140
|
+
node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例
|
|
141
|
+
node smoke\cordis-host-test.mjs # 真 cordis 端到端(找不到本机 DSH 时自动跳过)
|
|
142
|
+
npm pack --dry-run # 预览发布包
|
|
131
143
|
```
|
|
132
144
|
|
|
145
|
+
`cordis-host-test.mjs` 会从本机 DSH 安装借 `@deepseek-ai/cordis`;源码 checkout 里没有 profile 依赖时,用 `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js` 指定即可。
|
|
146
|
+
|
|
133
147
|
## License
|
|
134
148
|
|
|
135
149
|
MIT — 见 [LICENSE](LICENSE)。
|
package/lib/client.js
CHANGED
|
@@ -12,16 +12,26 @@
|
|
|
12
12
|
// What this plugin does (all client-side, no settings transport needed):
|
|
13
13
|
// - watches two services, because pending interactions and the running bit
|
|
14
14
|
// have different owners on current hosts:
|
|
15
|
-
// * "uiSession" ->
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
15
|
+
// * "uiSession" -> the pending-interaction source, whose *store* changed
|
|
16
|
+
// shape once and is therefore read through one adapter:
|
|
17
|
+
// - DSH 0.1.2-alpha.2 .. 0.1.5-rc.3: `pendingInteractions`, a
|
|
18
|
+
// Map<sessionId, interaction> whose values carry kind
|
|
19
|
+
// 'approval' | 'plan-review' | 'question' (the controller snapshots
|
|
20
|
+
// stopped carrying `pending` in 0.1.2-alpha.2, so that store was the
|
|
21
|
+
// only live source back then);
|
|
22
|
+
// - DSH 0.1.6-alpha.2 and later (including 0.2.0-rc.2):
|
|
23
|
+
// `sessionStatus`, a Map<sessionId, {running, pendingInteraction,
|
|
24
|
+
// completionUnread}>; the old store is gone, so this is the only
|
|
25
|
+
// live source on those hosts.
|
|
26
|
+
// Both are resolved through ctx.get() — cordis throws on a plain
|
|
21
27
|
// ctx.uiSession read for any name absent from the plugin's inject map;
|
|
22
28
|
// * "sessions" -> the selected session's ConversationSnapshot (running)
|
|
23
29
|
// and the session-list summaries of every other session (running /
|
|
24
|
-
// completed), which carry the "reply finished" edge on every version
|
|
30
|
+
// completed), which carry the "reply finished" edge on every version.
|
|
31
|
+
// The selected ("current") session is read from the list snapshot's
|
|
32
|
+
// `current` field where it exists (<= 0.1.5-rc.3) and otherwise derived
|
|
33
|
+
// from the mainView retention count (0.1.6+, the way the shipped
|
|
34
|
+
// DocumentTitle / ui-session pick it);
|
|
25
35
|
// - fires a Windows desktop Notification (+ optional sound) when the agent
|
|
26
36
|
// needs the user's input — pending interaction kinds 'approval',
|
|
27
37
|
// 'plan-review' or 'question' — and when a reply/turn finishes;
|
|
@@ -172,18 +182,22 @@ window.__ModuleLoader__.load({
|
|
|
172
182
|
}
|
|
173
183
|
// Bring the window forward and make the alerted session the current one,
|
|
174
184
|
// so a click on the toast lands on the conversation that needs the user
|
|
175
|
-
// instead of just the app.
|
|
176
|
-
//
|
|
177
|
-
//
|
|
185
|
+
// instead of just the app. Navigation moved with the host API: `sessions`
|
|
186
|
+
// exposed open(id) through 0.1.5-rc.3, and 0.1.6+ routes the same action
|
|
187
|
+
// through `uiWorkspace.openSession(id)`. Every failure here is non-fatal: a
|
|
188
|
+
// session that left the list, an older host without either verb, or a
|
|
189
|
+
// cordis service that cannot be resolved must still leave a usable window
|
|
190
|
+
// in front.
|
|
178
191
|
function focusSession(sessionId) {
|
|
179
192
|
try { window.focus(); } catch (e) {}
|
|
180
193
|
if (!sessionId) return;
|
|
181
194
|
try {
|
|
182
|
-
var svc = lookupService("sessions");
|
|
183
|
-
if (!svc || typeof svc.open !== "function") return;
|
|
184
195
|
var st = listStore && listStore.getSnapshot();
|
|
185
|
-
if (st && st.byId && !st.byId[sessionId] &&
|
|
186
|
-
svc
|
|
196
|
+
if (st && st.byId && !st.byId[sessionId] && currentSessionId() !== sessionId) return;
|
|
197
|
+
var svc = lookupService("sessions");
|
|
198
|
+
if (svc && typeof svc.open === "function") { svc.open(sessionId); return; }
|
|
199
|
+
var ws = lookupService("uiWorkspace");
|
|
200
|
+
if (ws && typeof ws.openSession === "function") ws.openSession(sessionId);
|
|
187
201
|
} catch (e) {}
|
|
188
202
|
}
|
|
189
203
|
function showToast(kind, title, body, sessionId) {
|
|
@@ -193,6 +207,11 @@ window.__ModuleLoader__.load({
|
|
|
193
207
|
body: body || "",
|
|
194
208
|
tag: "dsh-notify-me-" + kind,
|
|
195
209
|
renotify: true,
|
|
210
|
+
// The plugin owns the audio: its WebAudio cues already differ per
|
|
211
|
+
// kind (attention vs done) and answer the Sound toggle and volume
|
|
212
|
+
// slider. Without this flag the platform plays its own chime on top
|
|
213
|
+
// of the beep — two sounds stacked on one alert.
|
|
214
|
+
silent: true,
|
|
196
215
|
icon: (location.origin || "") + "/favicon.svg"
|
|
197
216
|
});
|
|
198
217
|
if (config.autoFocus) {
|
|
@@ -382,11 +401,24 @@ window.__ModuleLoader__.load({
|
|
|
382
401
|
return raw;
|
|
383
402
|
}
|
|
384
403
|
// The session the user is looking at, or null while the list is not ready.
|
|
404
|
+
// Two host generations own this fact differently:
|
|
405
|
+
// * <= 0.1.5-rc.3: the list snapshot carried `current`;
|
|
406
|
+
// * 0.1.6+ (incl. 0.2.0-rc.2): `current` is gone and the main view's
|
|
407
|
+
// retention count marks the on-screen session — the shipped
|
|
408
|
+
// DocumentTitle, ui-workspace browser and ui-session isMain() all
|
|
409
|
+
// derive it exactly this way.
|
|
385
410
|
function currentSessionId() {
|
|
386
411
|
try {
|
|
387
412
|
var st = listStore && listStore.getSnapshot();
|
|
388
|
-
|
|
389
|
-
|
|
413
|
+
if (!st) return null;
|
|
414
|
+
if (typeof st.current === "string" && st.current) return st.current;
|
|
415
|
+
var byId = st.byId || {};
|
|
416
|
+
for (var id in byId) {
|
|
417
|
+
var row = byId[id];
|
|
418
|
+
if (row && row.retainedBy && (row.retainedBy.mainView || 0) > 0) return id;
|
|
419
|
+
}
|
|
420
|
+
} catch (e) {}
|
|
421
|
+
return null;
|
|
390
422
|
}
|
|
391
423
|
// Display label for a session id we only know from the interaction map:
|
|
392
424
|
// the list row's title, the localized "current" placeholder for the selected
|
|
@@ -397,7 +429,7 @@ window.__ModuleLoader__.load({
|
|
|
397
429
|
if (st) {
|
|
398
430
|
var sum = st.byId && st.byId[sid];
|
|
399
431
|
if (sum) return sessionLabelText({ label: sum.displayTitle || sum.title || "" });
|
|
400
|
-
if (
|
|
432
|
+
if (currentSessionId() === sid) return texts().current;
|
|
401
433
|
}
|
|
402
434
|
} catch (e) {}
|
|
403
435
|
return sid;
|
|
@@ -453,6 +485,7 @@ window.__ModuleLoader__.load({
|
|
|
453
485
|
var uiPending = false; // true once the interaction snapshot is readable
|
|
454
486
|
var uiSub = null;
|
|
455
487
|
var uiStore = null; // the interaction store we hold a subscription on
|
|
488
|
+
var uiSource = null; // 'sessionStatus' (0.1.6+) | 'pendingInteractions' (0.1.2-0.1.5)
|
|
456
489
|
var uiBindNote = "not attempted"; // last lookup outcome, read by __dshNotifyMe.debug()
|
|
457
490
|
var uiWarned = false; // one unreachable-service warning per page load
|
|
458
491
|
var uiBaseline = Object.create(null); // sessionId -> { key: string }
|
|
@@ -614,8 +647,7 @@ window.__ModuleLoader__.load({
|
|
|
614
647
|
function bindCurrentFace() {
|
|
615
648
|
if (!listStore) return;
|
|
616
649
|
try {
|
|
617
|
-
var
|
|
618
|
-
var cur = st && st.current;
|
|
650
|
+
var cur = currentSessionId();
|
|
619
651
|
if (!cur) { detachFace(); return; }
|
|
620
652
|
if (cur === currentFaceId && faceSub) return;
|
|
621
653
|
detachFace();
|
|
@@ -645,7 +677,7 @@ window.__ModuleLoader__.load({
|
|
|
645
677
|
if (!listStore) return;
|
|
646
678
|
try {
|
|
647
679
|
var st = listStore.getSnapshot();
|
|
648
|
-
var cur =
|
|
680
|
+
var cur = currentSessionId();
|
|
649
681
|
bindCurrentFace();
|
|
650
682
|
bindUiSession();
|
|
651
683
|
var byId = (st && st.byId) || {};
|
|
@@ -661,10 +693,17 @@ window.__ModuleLoader__.load({
|
|
|
661
693
|
|
|
662
694
|
// ─────────────── uiSession pending-interaction watcher ───────────
|
|
663
695
|
// DSH >= 0.1.2-alpha.2 moved pending interactions out of the Controller
|
|
664
|
-
// snapshots into their own store
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
696
|
+
// snapshots into their own uiSession store, and that store changed shape
|
|
697
|
+
// once, so both readings share one adapter:
|
|
698
|
+
// * 0.1.2-alpha.2 .. 0.1.5-rc.3: `pendingInteractions`, a
|
|
699
|
+
// Map<sessionId, interaction> whose values carry kind + key + sessionId
|
|
700
|
+
// and the domain's own detail fields;
|
|
701
|
+
// * 0.1.6-alpha.2 and later (incl. 0.2.0-rc.2): `sessionStatus`, a
|
|
702
|
+
// Map<sessionId, {running, pendingInteraction, completionUnread}>; the
|
|
703
|
+
// old store no longer exists, so reading it is what made this channel
|
|
704
|
+
// dead on every 0.1.6+/0.2 host.
|
|
705
|
+
// Either store is the only live "the agent is waiting on you" source on its
|
|
706
|
+
// host generation.
|
|
668
707
|
//
|
|
669
708
|
// The service is looked up lazily rather than declared in `inject`:
|
|
670
709
|
// a cordis `inject` entry is a hard gate (the entry never activates while
|
|
@@ -695,37 +734,54 @@ window.__ModuleLoader__.load({
|
|
|
695
734
|
try { if (rootCtx && name in rootCtx) svc = rootCtx[name] || null; } catch (e) { svc = null; }
|
|
696
735
|
return svc;
|
|
697
736
|
}
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
737
|
+
// Is this an observable snapshot store (getSnapshot + subscribe)?
|
|
738
|
+
function isStore(s) {
|
|
739
|
+
return !!s && typeof s.getSnapshot === "function" && typeof s.subscribe === "function";
|
|
740
|
+
}
|
|
741
|
+
// Pick the host's pending-interaction store: sessionStatus wherever it
|
|
742
|
+
// exists (0.1.6+), else the legacy pendingInteractions map (0.1.2-0.1.5).
|
|
743
|
+
function interactionStore(svc) {
|
|
744
|
+
if (svc && isStore(svc.sessionStatus)) return { store: svc.sessionStatus, source: "sessionStatus" };
|
|
745
|
+
if (svc && isStore(svc.pendingInteractions)) return { store: svc.pendingInteractions, source: "pendingInteractions" };
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
// One pending interaction out of one store value; null when this session
|
|
749
|
+
// currently has none.
|
|
750
|
+
function pendingOf(value, source) {
|
|
751
|
+
if (!value) return null;
|
|
752
|
+
if (source === "sessionStatus") return value.pendingInteraction || null;
|
|
753
|
+
return value;
|
|
707
754
|
}
|
|
755
|
+
// Record the last lookup outcome for debug() and warn once on the only
|
|
756
|
+
// outcome that is a defect rather than a host generation.
|
|
708
757
|
function noteUiBind(note) {
|
|
709
758
|
if (uiBindNote === note) return;
|
|
710
759
|
uiBindNote = note;
|
|
711
|
-
|
|
760
|
+
// Two notes are normal and stay silent: "not visible" is either a
|
|
761
|
+
// pre-0.1.2 host or the ordinary boot race (this entry may apply before
|
|
762
|
+
// the uiSession entry activates; the 1.5s retry timer binds it a moment
|
|
763
|
+
// later), and "bound" is success. A service instance we can hold but
|
|
764
|
+
// whose shape we do not recognize is the case worth one warning.
|
|
765
|
+
if (note === "bound" || uiWarned || note === "uiSession not visible") return;
|
|
712
766
|
uiWarned = true;
|
|
713
767
|
try {
|
|
714
|
-
console.warn("[dsh-notify-me] uiSession is
|
|
768
|
+
console.warn("[dsh-notify-me] uiSession is reachable but exposes no pending-interaction source (" + note + ") — approval alerts are silent; please report this against DSH with window.__dshNotifyMe.debug()");
|
|
715
769
|
} catch (e) {}
|
|
716
770
|
}
|
|
717
771
|
function bindUiSession() {
|
|
718
772
|
var svc = lookupService("uiSession");
|
|
719
|
-
var
|
|
720
|
-
if (!
|
|
721
|
-
noteUiBind(svc ? "uiSession has
|
|
773
|
+
var found = interactionStore(svc);
|
|
774
|
+
if (!found) {
|
|
775
|
+
noteUiBind(svc ? "uiSession has neither sessionStatus nor pendingInteractions" : "uiSession not visible");
|
|
722
776
|
return;
|
|
723
777
|
}
|
|
778
|
+
var store = found.store;
|
|
724
779
|
if (uiSub && uiStore === store) { noteUiBind("bound"); return; }
|
|
725
|
-
// the service instance was swapped (plugin HMR): drop the
|
|
726
|
-
// subscription and its marks, then bind the live store
|
|
780
|
+
// the service instance (or its store) was swapped (plugin HMR): drop the
|
|
781
|
+
// stale subscription and its marks, then bind the live store
|
|
727
782
|
if (uiSub) detachUiSession();
|
|
728
783
|
uiStore = store;
|
|
784
|
+
uiSource = found.source;
|
|
729
785
|
uiPending = true; // this watcher now owns the title marker
|
|
730
786
|
uiSub = store.subscribe(function () { try { evalUiPending(); } catch (e) {} });
|
|
731
787
|
noteUiBind("bound");
|
|
@@ -733,12 +789,15 @@ window.__ModuleLoader__.load({
|
|
|
733
789
|
}
|
|
734
790
|
function evalUiPending() {
|
|
735
791
|
var svc = lookupService("uiSession");
|
|
736
|
-
var
|
|
737
|
-
if (!
|
|
738
|
-
var snap = store.getSnapshot(); // Map<sessionId, interaction>
|
|
792
|
+
var found = interactionStore(svc);
|
|
793
|
+
if (!found || typeof found.store.getSnapshot !== "function") return;
|
|
794
|
+
var snap = found.store.getSnapshot(); // Map<sessionId, interaction|status>
|
|
739
795
|
if (!snap || typeof snap.forEach !== "function") return;
|
|
740
796
|
var seen = Object.create(null);
|
|
741
|
-
snap.forEach(function (
|
|
797
|
+
snap.forEach(function (value, sid) {
|
|
798
|
+
var it = pendingOf(value, found.source);
|
|
799
|
+
// A session with no pending interaction is not "seen": leaving it out
|
|
800
|
+
// lets the teardown pass below release any mark its answer replaced.
|
|
742
801
|
if (!it) return;
|
|
743
802
|
seen[sid] = true;
|
|
744
803
|
var key = it.key || (it.kind + ":" + sid);
|
|
@@ -767,6 +826,7 @@ window.__ModuleLoader__.load({
|
|
|
767
826
|
function detachUiSession() {
|
|
768
827
|
if (uiSub) { try { uiSub(); } catch (e) {} uiSub = null; }
|
|
769
828
|
uiStore = null;
|
|
829
|
+
uiSource = null;
|
|
770
830
|
// drop every mark this watcher holds; hosts without the interaction store
|
|
771
831
|
// go back to the controller marker path
|
|
772
832
|
for (var s in uiBaseline) unmarkAttention(uiBaseline[s].key);
|
|
@@ -788,7 +848,7 @@ window.__ModuleLoader__.load({
|
|
|
788
848
|
});
|
|
789
849
|
} catch (e) {}
|
|
790
850
|
// keep in sync with package.json "version" on every release
|
|
791
|
-
api.version = "1.1
|
|
851
|
+
api.version = "1.2.1";
|
|
792
852
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
793
853
|
api.resetConfig = function () {
|
|
794
854
|
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
@@ -804,9 +864,10 @@ window.__ModuleLoader__.load({
|
|
|
804
864
|
return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ")";
|
|
805
865
|
};
|
|
806
866
|
// Live diagnostics for the "why did nothing pop" case. The needs-input
|
|
807
|
-
// channel has
|
|
808
|
-
//
|
|
809
|
-
//
|
|
867
|
+
// channel has three possible owners on current hosts (uiSession.sessionStatus
|
|
868
|
+
// on 0.1.6+, uiSession.pendingInteractions on 0.1.2-0.1.5, the controller
|
|
869
|
+
// snapshots before that) and each one failed silently in turn, so both the
|
|
870
|
+
// binding state and which store answered have to be readable from the page.
|
|
810
871
|
api.debug = function () {
|
|
811
872
|
return {
|
|
812
873
|
version: api.version,
|
|
@@ -821,6 +882,8 @@ window.__ModuleLoader__.load({
|
|
|
821
882
|
visibility: hidden() ? "hidden" : "visible",
|
|
822
883
|
uiSession: uiPending ? "bound" : "unbound",
|
|
823
884
|
uiSessionNote: uiBindNote,
|
|
885
|
+
uiSessionSource: uiSource,
|
|
886
|
+
currentSession: currentSessionId(),
|
|
824
887
|
sessionBaselines: Object.keys(prevById).length,
|
|
825
888
|
pendingBaselines: Object.keys(uiBaseline).length,
|
|
826
889
|
firedKeys: Object.keys(attentionKeysFired),
|
|
@@ -1072,9 +1135,10 @@ window.__ModuleLoader__.load({
|
|
|
1072
1135
|
// a cordis inject entry is a hard gate (the entry stays pending while the
|
|
1073
1136
|
// service is missing, and the web boot audit fails on entries that never
|
|
1074
1137
|
// activate), while hosts older than 0.1.2-alpha.2 have no uiSession at all.
|
|
1075
|
-
// It is therefore looked up lazily in bindUiSession()
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1138
|
+
// It is therefore looked up lazily in bindUiSession() — as are `uiWorkspace`
|
|
1139
|
+
// (toast-click navigation on 0.1.6+) and `sessions.open` (its pre-0.1.6
|
|
1140
|
+
// predecessor) — with the controller path above as the fallback. The bundle
|
|
1141
|
+
// entry "@deepseek-ai/dsh-client-ui-session" stays in package.json's
|
|
1078
1142
|
// dsh.client.inject so the load order keeps the bundle in the graph.
|
|
1079
1143
|
var inject = ["sessions", "locale", "slots"];
|
|
1080
1144
|
|
|
@@ -1093,8 +1157,11 @@ window.__ModuleLoader__.load({
|
|
|
1093
1157
|
// is present. The reminder core above never depends on this.
|
|
1094
1158
|
try {
|
|
1095
1159
|
if (React && React.createElement && ctx.slots && ctx.locale && typeof ctx.locale.register === "function" && typeof ctx.slots.inject === "function") {
|
|
1160
|
+
// The effect returns register()'s own disposer, so a client-plugin
|
|
1161
|
+
// reload (HMR) unregisters the dictionaries before re-registering
|
|
1162
|
+
// them; locale.register throws when a namespace already has a locale.
|
|
1096
1163
|
ctx.effect(function () {
|
|
1097
|
-
ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
|
|
1164
|
+
return ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
|
|
1098
1165
|
}, "dsh-notify-me: settings dictionaries");
|
|
1099
1166
|
var t = ctx.locale.bind(NS);
|
|
1100
1167
|
ctx.slots.inject("settings.section", function () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-me",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "DSH 桌面消息提醒插件:模型需要你操作(审批/方案确认/提问的可操作提醒)或回复在后台完成时,系统通知+提示音+标签页标题提醒你;可在设置页开关并切换通知语言。Desktop notification & message alerts for DeepSeek Harness web — toasts, sounds and a tab-title marker when the agent needs your input (approval / plan review / question) or a reply finishes in the background; toggleable from Settings with zh/en notification language.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -80,12 +80,13 @@
|
|
|
80
80
|
"0.1.1-rc.2": "compatible",
|
|
81
81
|
"0.1.2-rc.1": "compatible",
|
|
82
82
|
"0.1.5-rc.1": "compatible",
|
|
83
|
-
"0.1.5-rc.2": "compatible"
|
|
83
|
+
"0.1.5-rc.2": "compatible",
|
|
84
|
+
"0.2.0-rc.2": "compatible"
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
},
|
|
87
88
|
"peerDependencies": {
|
|
88
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1"
|
|
89
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || >=0.1.6-alpha.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
89
90
|
},
|
|
90
91
|
"peerDependenciesMeta": {
|
|
91
92
|
"@deepseek-ai/dsh-client-locale": {
|