dsh-notify-me 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to **dsh-notify-me** are documented here.
4
4
 
5
+ ## [1.5.1] — 2026-10-04
6
+
7
+ ### 修复
8
+ - **「点击通知回到对应对话」在带按钮的审批通知上补齐窗口调度**:这类通知由 Service Worker 弹出,此前 worker 只把点击转告页面,窗口能不能到前台全押在页面消息回调里的 `window.focus()` 上——那段代码不在用户点击的手势上下文里,Windows 前台抢占规则下窗口可能根本抬不起来。现在 worker 在 `notificationclick` 里按规范用 `client.focus()` 调起窗口。
9
+ - **多标签页不再互相抢焦点、一起乱切对话**:worker 的点击转信此前广播给所有同源 DSH 标签页,每个开着的标签页都会聚焦自己并各自切对话。现在一次点击只挑一个窗口(优先当前可见的)调起并切对话,其余窗口只收「不导航」的副本、留在原地;「同意 / 拒绝」按钮照旧只裁决、不调窗口。
10
+ - 「点击通知回到对应对话」关掉后的承诺在持久通知上也成立:show 时把该设置记进通知 `data`,worker 据此不调任何窗口;旧版本弹出的遗留通知没有该字段,维持此前行为。
11
+
12
+ ### 变更
13
+ - 版本号 1.5.0 → 1.5.1;`window.__dshNotifyMe.version` 同步。
14
+ - README(中英)同步:「点击通知回到对应对话」补多窗口口径,「工作原理」补 worker 侧的窗口调度说明。
15
+
16
+ ### 测试
17
+ - `smoke/smoke-test.cjs` 快捷裁决桥用例补两个 window client 的点击路由断言:点正文只调起可见窗口并只让它切对话(其余客户端收 `navigate: false` 不动)、点正文不裁决;点按钮不调任何窗口也不切对话;`autoFocus` 关掉时通知 `data.focus` 记 `false`、点击既不调窗口也不切对话。
18
+
5
19
  ## [1.5.0] — 2026-10-03
6
20
 
7
21
  ### 新增
package/README.en.md CHANGED
@@ -56,7 +56,7 @@ Open **Settings → Notify me** (refresh the page once after installing):
56
56
  - **"Needs you" alerts while the page is open** (default on) and **"Reply finished" alerts while the page is open** (default off);
57
57
  - **Stay quiet for the conversation on screen** (default on): a wait inside the conversation you are looking at keeps only the tab marker — no toast or sound landing on the approval card. Backgrounding the page puts that alert back at once, unless you already handled it. Waits in other background sessions are unaffected;
58
58
  - **Ignore subagent sessions** (default on): background subagent child sessions raise neither "needs you" nor "reply finished" — those are steps of the parent conversation's turn, and the parent's own alert already covers them. The subagent session you have open alerts as usual. Turn it off to alert for every session;
59
- - **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from; when off, clicking a toast does nothing;
59
+ - **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from. With several DSH tabs open, one click raises exactly one window (a visible one first) and switches only that one — the others stay put; when off, clicking a toast does nothing;
60
60
  - **Decide approvals from the toast** (default on): approval notifications carry Approve / Reject buttons, and one click settles the request exactly like the Allow once / Reject buttons on the approval card. Clicking the toast body still takes you back to the conversation;
61
61
  - **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
62
62
  - **Test buttons** — send one "needs you", "reply finished" or "approval buttons" test alert with the current settings (**not** limited by the "while the page is open" toggles; the "needs you" test's title marker clears itself after ~6s).
@@ -143,7 +143,7 @@ The reminder core stays dependency-free and self-contained: notification copy re
143
143
 
144
144
  ### Quick decisions, and how they reach the agent
145
145
 
146
- Action buttons (`actions`) belong to persistent notifications shown through `ServiceWorkerRegistration.showNotification()`; passing `actions` to the `new Notification()` constructor throws a `TypeError`. That is a browser rule no plugin can route around, so the bundle holds two jobs in the same bytes: the page half registers the reminder factory as it always did, and the very same file is registered as a Service Worker whose only task is relaying which button was clicked back to the page. The decision itself happens in the page, through the same `PendingApproval.answer('allowed-once' | 'rejected')` the approval card uses — which is why "Approve" is exactly "Allow once" and nothing broader.
146
+ Action buttons (`actions`) belong to persistent notifications shown through `ServiceWorkerRegistration.showNotification()`; passing `actions` to the `new Notification()` constructor throws a `TypeError`. That is a browser rule no plugin can route around, so the bundle holds two jobs in the same bytes: the page half registers the reminder factory as it always did, and the very same file is registered as a Service Worker whose only task is relaying which button was clicked back to the page. The decision itself happens in the page, through the same `PendingApproval.answer('allowed-once' | 'rejected')` the approval card uses — which is why "Approve" is exactly "Allow once" and nothing broader. The "raise a window and switch its conversation" part of a body click is scheduled on the worker half too: `client.focus()` raises exactly one window (a visible one first) and routes the conversation switch to that window alone, so several open DSH tabs never fight over focus or all switch at once.
147
147
 
148
148
  Every button toast is booked against its interaction key, so it can only decide the request it was raised for. Once the page answers that request or a newer one replaces it, clicking the leftover buttons decides nothing and just closes the notification. Button toasts also skip the auto-dismiss timer: an undecided approval stays in the notification center until you settle it.
149
149
 
package/README.md CHANGED
@@ -56,7 +56,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
56
56
  - **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
57
57
  - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标题标记,不弹通知也不响音(免得盖住审批卡片);页面切到后台立刻补发,若你已经处理掉就作废。其它后台会话不受影响;
58
58
  - **子代理会话不提醒**(默认开):后台子代理(subagent)子会话的「需要你」与「回复完成」都不提醒——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;关掉后恢复逐会话提醒;
59
- - **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;关掉后点通知不做任何事;
59
+ - **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;多个 DSH 标签页同时开着时,一次点击只调起一个窗口(优先当前可见的那个)并只切它的对话,其余标签页不动;关掉后点通知不做任何事;
60
60
  - **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」;
61
61
  - **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
62
62
  - **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
@@ -139,7 +139,7 @@ window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:
139
139
 
140
140
  ### 快捷裁决怎么走通的
141
141
 
142
- 通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')`,所以「同意」就是「允许一次」,不多放行任何东西。
142
+ 通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')`,所以「同意」就是「允许一次」,不多放行任何东西。点正文的「调窗口 + 切对话」同样归 worker 这半侧调度:它用 `client.focus()` 只调起一个窗口(优先当前可见的),并只让那个窗口切对话——多个 DSH 标签页不会互相抢焦点或一起乱切。
143
143
 
144
144
  每条按钮通知按交互 key 记账,一次只认领一条等待;请求被页面里答掉、或被新请求顶替之后,残留通知上的按钮再点也不会误裁,只是安静地关掉。带按钮的通知不设自动消失——审批没裁决就一直留在通知中心里等你。
145
145
 
package/lib/client.js CHANGED
@@ -55,7 +55,8 @@
55
55
  // card uses. Action buttons only exist on persistent notifications, so this
56
56
  // file is dual-context: registered as a Service Worker as well, whose
57
57
  // notificationclick relays the chosen button back to this page (broadcast +
58
- // client postMessage). Where a worker cannot run (insecure context, no
58
+ // client postMessage) and, on a body click, raises exactly one window
59
+ // (client.focus()) and routes the conversation switch to that window alone. Where a worker cannot run (insecure context, no
59
60
  // bundle URL) the plugin degrades to today's plain toast;
60
61
  // - registers a "通知提醒 / Notify me" page into DSH Settings
61
62
  // (settings.section slot) exposing an on/off master switch, per-channel
@@ -103,22 +104,92 @@
103
104
  }
104
105
  } catch (e) {}
105
106
  }
107
+ // One relayed click; `navigate` says whether the receiving page may
108
+ // steer its window (raise it + switch to the alerted conversation).
109
+ function clickMsg(base, navigate) {
110
+ return {
111
+ source: base.source,
112
+ type: base.type,
113
+ action: base.action,
114
+ key: base.key,
115
+ sessionId: base.sessionId,
116
+ navigate: navigate
117
+ };
118
+ }
119
+ // A body click means "take me there", and both halves of that request are
120
+ // this handler's job. The window raise belongs here because only a
121
+ // notificationclick carries the click's user activation — the page-side
122
+ // window.focus() a plain relay triggers afterwards can lose the OS
123
+ // foreground race and leave DSH behind other windows. So exactly one
124
+ // window client is picked (a visible one first), that client alone gets
125
+ // `navigate: true` plus the focus() call, and every other recipient gets
126
+ // `navigate: false` and stays put: several open DSH tabs must not fight
127
+ // over focus or all switch their own conversation at once. Buttons stay
128
+ // hands-off — deciding from the toast must not drag a window up. The
129
+ // `data.focus` flag (the autoFocus setting at show time) keeps the
130
+ // "off = clicking does nothing" promise even though a worker cannot read
131
+ // the page's config; toasts from older versions carry no flag and keep
132
+ // the previous best-effort behavior.
133
+ function deliverClick(base, wantsFocus) {
134
+ var send = function (list) {
135
+ list = list || [];
136
+ var best = null;
137
+ if (wantsFocus) {
138
+ for (var i = 0; i < list.length; i++) {
139
+ var c = list[i];
140
+ if (!c || typeof c.postMessage !== "function") continue;
141
+ if (c.visibilityState === "visible") { best = c; break; }
142
+ if (!best) best = c;
143
+ }
144
+ }
145
+ for (var j = 0; j < list.length; j++) {
146
+ try { list[j].postMessage(clickMsg(base, wantsFocus && list[j] === best)); } catch (e) {}
147
+ }
148
+ try {
149
+ if (typeof g.BroadcastChannel === "function") {
150
+ var ch = new g.BroadcastChannel(BRIDGE_CHANNEL);
151
+ ch.postMessage(clickMsg(base, false));
152
+ try { ch.close(); } catch (e) {}
153
+ }
154
+ } catch (e) {}
155
+ if (best && typeof best.focus === "function") {
156
+ try {
157
+ var fp = best.focus();
158
+ if (fp && typeof fp.then === "function") fp.then(null, function () {});
159
+ } catch (e) {}
160
+ }
161
+ };
162
+ try {
163
+ if (g.clients && typeof g.clients.matchAll === "function") {
164
+ var p = g.clients.matchAll({ type: "window", includeUncontrolled: true });
165
+ if (p && typeof p.then === "function") {
166
+ p.then(send, function () { relay(clickMsg(base, wantsFocus)); });
167
+ return;
168
+ }
169
+ }
170
+ } catch (e) {}
171
+ relay(clickMsg(base, wantsFocus));
172
+ }
106
173
  g.addEventListener("notificationclick", function (event) {
107
174
  try {
108
175
  var n = event.notification;
109
176
  var data = (n && n.data) || {};
110
- var msg = {
177
+ var action = event.action || "";
178
+ var base = {
111
179
  source: "dsh-notify-me",
112
180
  type: "notification",
113
- action: event.action || "",
181
+ action: action,
114
182
  key: data.key || null,
115
183
  sessionId: data.sessionId || null
116
184
  };
117
185
  try { if (n && typeof n.close === "function") n.close(); } catch (e) {}
186
+ var run = action
187
+ ? function () { relay(base); }
188
+ : function () { deliverClick(base, data.focus !== false); };
118
189
  if (typeof event.waitUntil === "function") {
119
- event.waitUntil(Promise.resolve().then(function () { relay(msg); }));
190
+ event.waitUntil(Promise.resolve().then(run));
120
191
  } else {
121
- relay(msg);
192
+ run();
122
193
  }
123
194
  } catch (e) {}
124
195
  });
@@ -163,7 +234,7 @@
163
234
  toast: true,
164
235
  sound: true,
165
236
  volume: 0.5,
166
- autoFocus: true, // clicking the toast focuses the DSH window
237
+ autoFocus: true, // clicking the toast raises DSH and switches to the alerted conversation
167
238
  // Approval toasts carry 同意/拒绝 buttons and settle the request on one
168
239
  // click (the same answer('allowed-once' | 'rejected') the in-page
169
240
  // approval card sends). Buttons need persistent notifications, served
@@ -415,6 +486,11 @@
415
486
  if (!msg || msg.source !== "dsh-notify-me" || msg.type !== "notification") return;
416
487
  if (msg.action === "approve") { decidePending(msg.key, "allowed-once"); return; }
417
488
  if (msg.action === "reject") { decidePending(msg.key, "rejected"); return; }
489
+ // The worker picks the one window a click should land on (see the bridge):
490
+ // its `navigate: false` copies keep every other DSH tab from stealing
491
+ // focus or switching its own conversation. A message from before that
492
+ // flag existed carries none and falls back to the old best-effort route.
493
+ if (msg.navigate === false) return;
418
494
  if (config.autoFocus) focusSession(msg.sessionId || null);
419
495
  }
420
496
  // The live interaction wins over the reference captured at alert time (the
@@ -515,7 +591,7 @@
515
591
  // The plugin owns the audio (see showToast's silent note below).
516
592
  silent: true,
517
593
  icon: (location.origin || "") + "/favicon.svg",
518
- data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test },
594
+ data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test, focus: config.autoFocus !== false },
519
595
  actions: [
520
596
  { action: "approve", title: d.approve },
521
597
  { action: "reject", title: d.reject }
@@ -1359,7 +1435,7 @@
1359
1435
  });
1360
1436
  } catch (e) {}
1361
1437
  // keep in sync with package.json "version" on every release
1362
- api.version = "1.5.0";
1438
+ api.version = "1.5.1";
1363
1439
  api.setConfig = function (patch) { return setConfigLocal(patch); };
1364
1440
  api.resetConfig = function () {
1365
1441
  try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-me",
3
- "version": "1.5.0",
3
+ "version": "1.5.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, Approve/Reject buttons on approval toasts to decide right from the notification; toggleable from Settings with zh/en notification language.",
5
5
  "license": "MIT",
6
6
  "type": "module",