dsh-notify-me 1.1.8 → 1.2.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/CHANGELOG.md +24 -0
- package/README.en.md +20 -6
- package/README.md +20 -6
- package/lib/client.js +114 -52
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] — 2026-09-30
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **`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`,直接显示这条通道绑的是哪个来源。
|
|
9
|
+
- **`0.1.6` 及以后的宿主上认不出「当前会话」**:会话列表快照不再有 `current`(`SessionListState` 只剩 `ids / byId / phase / projectionsBySession`),于是当前会话面(face)订阅从未建立、`onListChanged()` 还把当前会话当成后台会话评估——后果是 1.1.7 的「当前对话不弹通知」规则失效(审批卡片就在屏幕上,Toast 仍会盖上去),标签页标记的归属也随之错位。现在按宿主自己的口径推导:有 `current` 用它,没有就取 `retainedBy.mainView > 0` 的那一行(与内置 `DocumentTitle` / `ui-workspace` / `ui-session` 的判定一致)。
|
|
10
|
+
- **`0.1.6` 及以后点通知切不回对应会话**:`sessions.open(id)` 已被移除,导航改由 `uiWorkspace.openSession(id)` 承担;插件现在两个都试(`open` 优先,旧宿主行为不变),会话已不在列表时依旧不调用。`debug()` 新增 `currentSession` 便于核对。
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
- `sessionStatus` 适配完整:同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
- `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`。
|
|
17
|
+
- 版本号 1.1.8 → 1.2.0(新宿主代际);`window.__dshNotifyMe.version` 同步。
|
|
18
|
+
- 设置页字典注册改为把 `locale.register()` 返回的 disposer 交给 `ctx.effect`(与官方 `ui-approval` 写法一致),客户端插件 HMR 重载时不会因「同名 namespace 已注册」而丢掉设置页。
|
|
19
|
+
- `uiSession` 绑定的诊断降噪:真实 `0.2.0-rc.2` 页面上每次加载都会出现的那条 `uiSession is provided but not reachable` 警告其实是**启动竞态**(本插件的客户端条目常早于 `ui-session` 条目激活,此时 `ctx.get()` 按 cordis 的严格语义拿不到尚未激活的服务),并非缺陷。现在只有**能拿到服务实例、但它既没有 `sessionStatus` 也没有 `pendingInteractions`** 时才打一条警告(并提示附上 `debug()` 输出);「还没可见」保持安静,由 1.5s 重试定时器接管,`debug().uiSessionNote` 仍可查。
|
|
20
|
+
|
|
21
|
+
### 测试
|
|
22
|
+
- `smoke/smoke-test.cjs` 增加第三代宿主用例(`uiSession.sessionStatus`、列表无 `current`、无 `sessions.open`):审批/提问/方案确认文案与详情、去重、等待消失释放标记、挂载前已存在的等待提醒一次、当前对话静默 + 转后台补发、后台会话前台提醒、主开关、`retainedBy.mainView` 推导当前会话、点通知走 `uiWorkspace.openSession`、会话已消失不打开。
|
|
23
|
+
- `smoke/cordis-host-test.mjs` 增加真 cordis 的 `sessionStatus` 宿主(兄弟插件提供 `sessionStatus` 与 `uiWorkspace`,`sessions` 不带 `open`):断言订阅绑到 `sessionStatus`、审批只提醒一次且正文带工具名 + 理由、标题标记设置与释放、点通知调用 `uiWorkspace.openSession`。原有 `pendingInteractions` 宿主与无 `uiSession` 的旧宿主用例保留。
|
|
24
|
+
- 自检命令补 `DSH_CORDIS_PATH`:源码 checkout 里没有 profile 依赖时,用它把 `cordis-host-test.mjs` 指向 checkout 内的 `vendor/cordis/lib/index.js`(否则该用例按设计跳过)。
|
|
25
|
+
|
|
26
|
+
### 已知限制
|
|
27
|
+
- `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文不再附带回复摘要:宿主快照不再提供 `nodes`,摘要取不到;提醒本身照常触发。
|
|
28
|
+
|
|
5
29
|
## [1.1.8] — 2026-09-24
|
|
6
30
|
|
|
7
31
|
### 新增
|
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) {
|
|
@@ -382,11 +396,24 @@ window.__ModuleLoader__.load({
|
|
|
382
396
|
return raw;
|
|
383
397
|
}
|
|
384
398
|
// The session the user is looking at, or null while the list is not ready.
|
|
399
|
+
// Two host generations own this fact differently:
|
|
400
|
+
// * <= 0.1.5-rc.3: the list snapshot carried `current`;
|
|
401
|
+
// * 0.1.6+ (incl. 0.2.0-rc.2): `current` is gone and the main view's
|
|
402
|
+
// retention count marks the on-screen session — the shipped
|
|
403
|
+
// DocumentTitle, ui-workspace browser and ui-session isMain() all
|
|
404
|
+
// derive it exactly this way.
|
|
385
405
|
function currentSessionId() {
|
|
386
406
|
try {
|
|
387
407
|
var st = listStore && listStore.getSnapshot();
|
|
388
|
-
|
|
389
|
-
|
|
408
|
+
if (!st) return null;
|
|
409
|
+
if (typeof st.current === "string" && st.current) return st.current;
|
|
410
|
+
var byId = st.byId || {};
|
|
411
|
+
for (var id in byId) {
|
|
412
|
+
var row = byId[id];
|
|
413
|
+
if (row && row.retainedBy && (row.retainedBy.mainView || 0) > 0) return id;
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {}
|
|
416
|
+
return null;
|
|
390
417
|
}
|
|
391
418
|
// Display label for a session id we only know from the interaction map:
|
|
392
419
|
// the list row's title, the localized "current" placeholder for the selected
|
|
@@ -397,7 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
397
424
|
if (st) {
|
|
398
425
|
var sum = st.byId && st.byId[sid];
|
|
399
426
|
if (sum) return sessionLabelText({ label: sum.displayTitle || sum.title || "" });
|
|
400
|
-
if (
|
|
427
|
+
if (currentSessionId() === sid) return texts().current;
|
|
401
428
|
}
|
|
402
429
|
} catch (e) {}
|
|
403
430
|
return sid;
|
|
@@ -453,6 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
453
480
|
var uiPending = false; // true once the interaction snapshot is readable
|
|
454
481
|
var uiSub = null;
|
|
455
482
|
var uiStore = null; // the interaction store we hold a subscription on
|
|
483
|
+
var uiSource = null; // 'sessionStatus' (0.1.6+) | 'pendingInteractions' (0.1.2-0.1.5)
|
|
456
484
|
var uiBindNote = "not attempted"; // last lookup outcome, read by __dshNotifyMe.debug()
|
|
457
485
|
var uiWarned = false; // one unreachable-service warning per page load
|
|
458
486
|
var uiBaseline = Object.create(null); // sessionId -> { key: string }
|
|
@@ -614,8 +642,7 @@ window.__ModuleLoader__.load({
|
|
|
614
642
|
function bindCurrentFace() {
|
|
615
643
|
if (!listStore) return;
|
|
616
644
|
try {
|
|
617
|
-
var
|
|
618
|
-
var cur = st && st.current;
|
|
645
|
+
var cur = currentSessionId();
|
|
619
646
|
if (!cur) { detachFace(); return; }
|
|
620
647
|
if (cur === currentFaceId && faceSub) return;
|
|
621
648
|
detachFace();
|
|
@@ -645,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
645
672
|
if (!listStore) return;
|
|
646
673
|
try {
|
|
647
674
|
var st = listStore.getSnapshot();
|
|
648
|
-
var cur =
|
|
675
|
+
var cur = currentSessionId();
|
|
649
676
|
bindCurrentFace();
|
|
650
677
|
bindUiSession();
|
|
651
678
|
var byId = (st && st.byId) || {};
|
|
@@ -661,10 +688,17 @@ window.__ModuleLoader__.load({
|
|
|
661
688
|
|
|
662
689
|
// ─────────────── uiSession pending-interaction watcher ───────────
|
|
663
690
|
// DSH >= 0.1.2-alpha.2 moved pending interactions out of the Controller
|
|
664
|
-
// snapshots into their own store
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
691
|
+
// snapshots into their own uiSession store, and that store changed shape
|
|
692
|
+
// once, so both readings share one adapter:
|
|
693
|
+
// * 0.1.2-alpha.2 .. 0.1.5-rc.3: `pendingInteractions`, a
|
|
694
|
+
// Map<sessionId, interaction> whose values carry kind + key + sessionId
|
|
695
|
+
// and the domain's own detail fields;
|
|
696
|
+
// * 0.1.6-alpha.2 and later (incl. 0.2.0-rc.2): `sessionStatus`, a
|
|
697
|
+
// Map<sessionId, {running, pendingInteraction, completionUnread}>; the
|
|
698
|
+
// old store no longer exists, so reading it is what made this channel
|
|
699
|
+
// dead on every 0.1.6+/0.2 host.
|
|
700
|
+
// Either store is the only live "the agent is waiting on you" source on its
|
|
701
|
+
// host generation.
|
|
668
702
|
//
|
|
669
703
|
// The service is looked up lazily rather than declared in `inject`:
|
|
670
704
|
// a cordis `inject` entry is a hard gate (the entry never activates while
|
|
@@ -695,37 +729,54 @@ window.__ModuleLoader__.load({
|
|
|
695
729
|
try { if (rootCtx && name in rootCtx) svc = rootCtx[name] || null; } catch (e) { svc = null; }
|
|
696
730
|
return svc;
|
|
697
731
|
}
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
732
|
+
// Is this an observable snapshot store (getSnapshot + subscribe)?
|
|
733
|
+
function isStore(s) {
|
|
734
|
+
return !!s && typeof s.getSnapshot === "function" && typeof s.subscribe === "function";
|
|
735
|
+
}
|
|
736
|
+
// Pick the host's pending-interaction store: sessionStatus wherever it
|
|
737
|
+
// exists (0.1.6+), else the legacy pendingInteractions map (0.1.2-0.1.5).
|
|
738
|
+
function interactionStore(svc) {
|
|
739
|
+
if (svc && isStore(svc.sessionStatus)) return { store: svc.sessionStatus, source: "sessionStatus" };
|
|
740
|
+
if (svc && isStore(svc.pendingInteractions)) return { store: svc.pendingInteractions, source: "pendingInteractions" };
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
// One pending interaction out of one store value; null when this session
|
|
744
|
+
// currently has none.
|
|
745
|
+
function pendingOf(value, source) {
|
|
746
|
+
if (!value) return null;
|
|
747
|
+
if (source === "sessionStatus") return value.pendingInteraction || null;
|
|
748
|
+
return value;
|
|
707
749
|
}
|
|
750
|
+
// Record the last lookup outcome for debug() and warn once on the only
|
|
751
|
+
// outcome that is a defect rather than a host generation.
|
|
708
752
|
function noteUiBind(note) {
|
|
709
753
|
if (uiBindNote === note) return;
|
|
710
754
|
uiBindNote = note;
|
|
711
|
-
|
|
755
|
+
// Two notes are normal and stay silent: "not visible" is either a
|
|
756
|
+
// pre-0.1.2 host or the ordinary boot race (this entry may apply before
|
|
757
|
+
// the uiSession entry activates; the 1.5s retry timer binds it a moment
|
|
758
|
+
// later), and "bound" is success. A service instance we can hold but
|
|
759
|
+
// whose shape we do not recognize is the case worth one warning.
|
|
760
|
+
if (note === "bound" || uiWarned || note === "uiSession not visible") return;
|
|
712
761
|
uiWarned = true;
|
|
713
762
|
try {
|
|
714
|
-
console.warn("[dsh-notify-me] uiSession is
|
|
763
|
+
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
764
|
} catch (e) {}
|
|
716
765
|
}
|
|
717
766
|
function bindUiSession() {
|
|
718
767
|
var svc = lookupService("uiSession");
|
|
719
|
-
var
|
|
720
|
-
if (!
|
|
721
|
-
noteUiBind(svc ? "uiSession has
|
|
768
|
+
var found = interactionStore(svc);
|
|
769
|
+
if (!found) {
|
|
770
|
+
noteUiBind(svc ? "uiSession has neither sessionStatus nor pendingInteractions" : "uiSession not visible");
|
|
722
771
|
return;
|
|
723
772
|
}
|
|
773
|
+
var store = found.store;
|
|
724
774
|
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
|
|
775
|
+
// the service instance (or its store) was swapped (plugin HMR): drop the
|
|
776
|
+
// stale subscription and its marks, then bind the live store
|
|
727
777
|
if (uiSub) detachUiSession();
|
|
728
778
|
uiStore = store;
|
|
779
|
+
uiSource = found.source;
|
|
729
780
|
uiPending = true; // this watcher now owns the title marker
|
|
730
781
|
uiSub = store.subscribe(function () { try { evalUiPending(); } catch (e) {} });
|
|
731
782
|
noteUiBind("bound");
|
|
@@ -733,12 +784,15 @@ window.__ModuleLoader__.load({
|
|
|
733
784
|
}
|
|
734
785
|
function evalUiPending() {
|
|
735
786
|
var svc = lookupService("uiSession");
|
|
736
|
-
var
|
|
737
|
-
if (!
|
|
738
|
-
var snap = store.getSnapshot(); // Map<sessionId, interaction>
|
|
787
|
+
var found = interactionStore(svc);
|
|
788
|
+
if (!found || typeof found.store.getSnapshot !== "function") return;
|
|
789
|
+
var snap = found.store.getSnapshot(); // Map<sessionId, interaction|status>
|
|
739
790
|
if (!snap || typeof snap.forEach !== "function") return;
|
|
740
791
|
var seen = Object.create(null);
|
|
741
|
-
snap.forEach(function (
|
|
792
|
+
snap.forEach(function (value, sid) {
|
|
793
|
+
var it = pendingOf(value, found.source);
|
|
794
|
+
// A session with no pending interaction is not "seen": leaving it out
|
|
795
|
+
// lets the teardown pass below release any mark its answer replaced.
|
|
742
796
|
if (!it) return;
|
|
743
797
|
seen[sid] = true;
|
|
744
798
|
var key = it.key || (it.kind + ":" + sid);
|
|
@@ -767,6 +821,7 @@ window.__ModuleLoader__.load({
|
|
|
767
821
|
function detachUiSession() {
|
|
768
822
|
if (uiSub) { try { uiSub(); } catch (e) {} uiSub = null; }
|
|
769
823
|
uiStore = null;
|
|
824
|
+
uiSource = null;
|
|
770
825
|
// drop every mark this watcher holds; hosts without the interaction store
|
|
771
826
|
// go back to the controller marker path
|
|
772
827
|
for (var s in uiBaseline) unmarkAttention(uiBaseline[s].key);
|
|
@@ -788,7 +843,7 @@ window.__ModuleLoader__.load({
|
|
|
788
843
|
});
|
|
789
844
|
} catch (e) {}
|
|
790
845
|
// keep in sync with package.json "version" on every release
|
|
791
|
-
api.version = "1.
|
|
846
|
+
api.version = "1.2.0";
|
|
792
847
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
793
848
|
api.resetConfig = function () {
|
|
794
849
|
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
@@ -804,9 +859,10 @@ window.__ModuleLoader__.load({
|
|
|
804
859
|
return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ")";
|
|
805
860
|
};
|
|
806
861
|
// Live diagnostics for the "why did nothing pop" case. The needs-input
|
|
807
|
-
// channel has
|
|
808
|
-
//
|
|
809
|
-
//
|
|
862
|
+
// channel has three possible owners on current hosts (uiSession.sessionStatus
|
|
863
|
+
// on 0.1.6+, uiSession.pendingInteractions on 0.1.2-0.1.5, the controller
|
|
864
|
+
// snapshots before that) and each one failed silently in turn, so both the
|
|
865
|
+
// binding state and which store answered have to be readable from the page.
|
|
810
866
|
api.debug = function () {
|
|
811
867
|
return {
|
|
812
868
|
version: api.version,
|
|
@@ -821,6 +877,8 @@ window.__ModuleLoader__.load({
|
|
|
821
877
|
visibility: hidden() ? "hidden" : "visible",
|
|
822
878
|
uiSession: uiPending ? "bound" : "unbound",
|
|
823
879
|
uiSessionNote: uiBindNote,
|
|
880
|
+
uiSessionSource: uiSource,
|
|
881
|
+
currentSession: currentSessionId(),
|
|
824
882
|
sessionBaselines: Object.keys(prevById).length,
|
|
825
883
|
pendingBaselines: Object.keys(uiBaseline).length,
|
|
826
884
|
firedKeys: Object.keys(attentionKeysFired),
|
|
@@ -1072,9 +1130,10 @@ window.__ModuleLoader__.load({
|
|
|
1072
1130
|
// a cordis inject entry is a hard gate (the entry stays pending while the
|
|
1073
1131
|
// service is missing, and the web boot audit fails on entries that never
|
|
1074
1132
|
// 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
|
-
//
|
|
1133
|
+
// It is therefore looked up lazily in bindUiSession() — as are `uiWorkspace`
|
|
1134
|
+
// (toast-click navigation on 0.1.6+) and `sessions.open` (its pre-0.1.6
|
|
1135
|
+
// predecessor) — with the controller path above as the fallback. The bundle
|
|
1136
|
+
// entry "@deepseek-ai/dsh-client-ui-session" stays in package.json's
|
|
1078
1137
|
// dsh.client.inject so the load order keeps the bundle in the graph.
|
|
1079
1138
|
var inject = ["sessions", "locale", "slots"];
|
|
1080
1139
|
|
|
@@ -1093,8 +1152,11 @@ window.__ModuleLoader__.load({
|
|
|
1093
1152
|
// is present. The reminder core above never depends on this.
|
|
1094
1153
|
try {
|
|
1095
1154
|
if (React && React.createElement && ctx.slots && ctx.locale && typeof ctx.locale.register === "function" && typeof ctx.slots.inject === "function") {
|
|
1155
|
+
// The effect returns register()'s own disposer, so a client-plugin
|
|
1156
|
+
// reload (HMR) unregisters the dictionaries before re-registering
|
|
1157
|
+
// them; locale.register throws when a namespace already has a locale.
|
|
1096
1158
|
ctx.effect(function () {
|
|
1097
|
-
ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
|
|
1159
|
+
return ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
|
|
1098
1160
|
}, "dsh-notify-me: settings dictionaries");
|
|
1099
1161
|
var t = ctx.locale.bind(NS);
|
|
1100
1162
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|