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 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** — verified to actually activate on `0.1.2-rc.1` and `0.1.5-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`. Per-release declarations live in `package.json` under `dsh.compatibility.dshReleases`. If a plugin's client entry lists a package that a newer runtime no longer ships in `dsh.client.inject`, the entry parks at `pending (waiting for services: …)` and never runs — which is what broke 1.1.3 on `0.1.2-rc.1` and later.
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 browser half subscribes to the client `sessions` service — the same source the UI itself renders from:
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** `ConversationSnapshot`: a `running: true → false` edge means a reply finished; a new entry in `pending[]` of kind `approval` / `plan-review` / `question` means the agent is waiting on you (payload text is shown in the alert when available);
116
- - **every other listed session's** summary: a new `pendingInteraction`, or the `completed` edge (finished while not selected), alerts you about background work.
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 # offline state-machine smoke test (master switch + zh/en language cases)
134
- npm pack --dry-run # preview the published tarball
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
- **兼容性** — 实测**真正激活**(各自独立 profile 启动:设置里出现「通知提醒」分区,即插件的 `apply` 确实执行)于 `0.1.2-rc.1` 与 `0.1.5-rc.2`;更早的 `0.1.1-rc.2` 亦验证过。逐版本声明见 `package.json` 的 `dsh.compatibility.dshReleases`。注意一个坑:若 `dsh.client.inject` 里列了新版运行时已不再提供的包,客户端条目会停在 `pending (waiting for services: …)` 而永不执行——1.1.3 在 `0.1.2-rc.1` 及以后正是这样失效的。
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
- 浏览器半身订阅客户端 `sessions` 服务(与 UI 同一数据源):
117
+ 提醒核心按宿主代际取源(见上表),当前会话一律来自客户端 `sessions` 服务:
110
118
 
111
- - **当前会话** `ConversationSnapshot`:`running` true→false = 回复完成;`pending[]` 新增 `approval` / `plan-review` / `question` = 模型在等你(可行时在提醒里展示提问/审批内容);
112
- - **其它已列会话**摘要:出现新 `pendingInteraction`,或 `completed` 边沿(非选中状态下跑完)→ 后台工作提醒。
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
- npm pack --dry-run # 预览发布包
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" -> pendingInteractions (a Map<sessionId, interaction>
16
- // snapshot whose values carry kind 'approval' | 'plan-review' |
17
- // 'question'). Present since DSH 0.1.2-alpha.2; the controller
18
- // snapshots stopped carrying `pending` in the same release, so this is
19
- // the only live source of "the agent is waiting on you" on 0.1.2+.
20
- // It is resolved through ctx.get() — cordis throws on a plain
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. Every failure here is non-fatal: a session that
176
- // left the list, an older host without `open()`, or a cordis service that
177
- // cannot be resolved must still leave a usable window in front.
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] && st.current !== sessionId) return;
186
- svc.open(sessionId);
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
- return (st && st.current) || null;
389
- } catch (e) { return null; }
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 (st.current === sid) return texts().current;
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 st = listStore.getSnapshot();
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 = st && st.current;
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 (uiSession.pendingInteractions), a
665
- // Map<sessionId, interaction> whose values carry kind + key + sessionId and
666
- // the domain's own detail fields. It is the only live "the agent is waiting
667
- // on you" source on those hosts.
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
- // Did some entry on this host ever provide the service? Separates the
699
- // normal "older host has no interaction store" case (stay silent, the
700
- // controller path still works there) from "provided but unreachable",
701
- // which is a real defect and deserves one console warning.
702
- function serviceWasProvided(name) {
703
- try {
704
- var props = rootCtx && rootCtx.reflect && rootCtx.reflect.props;
705
- return !!(props && props[name]);
706
- } catch (e) { return false; }
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
- if (note === "bound" || uiWarned || !serviceWasProvided("uiSession")) return;
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 provided but not reachable (" + note + ") — approval alerts fall back to the legacy controller path, which stopped reporting pending interactions in DSH 0.1.2+");
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 store = svc && svc.pendingInteractions;
720
- if (!store || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
721
- noteUiBind(svc ? "uiSession has no pendingInteractions store" : "uiSession not visible");
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 stale
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 store = svc && svc.pendingInteractions;
737
- if (!store || typeof store.getSnapshot !== "function") return;
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 (it, sid) {
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.8";
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 two possible owners on current hosts (uiSession on 0.1.2+,
808
- // the controller snapshots before that) and both used to fail silently, so
809
- // the binding state has to be readable from the page itself.
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(), with the controller
1076
- // path above as the fallback. The bundle entry
1077
- // "@deepseek-ai/dsh-client-ui-session" stays in package.json's
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.8",
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": {