dsh-notify-me 1.10.3 → 1.10.5

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,30 @@
2
2
 
3
3
  All notable changes to **dsh-notify-me** are documented here.
4
4
 
5
+ ## [1.10.5] — 2026-10-21
6
+
7
+ ### 修复
8
+
9
+ - **桌面端窗口被遮住时「回复完成」不提醒**:本插件判断「你离开了」只看 `document.hidden` / `visibilityState`,而桌面壳里把 DSH 窗口切到别的应用后面、或被别的窗口盖住时,页面依然是 `visible` —— 只有最小化或整窗被遮挡才会变成 `hidden`。于是「只在后台提醒」的回复完成提醒在窗口只是被盖住时被静默丢弃(1.10.5 之前只在真正最小化时才响)。现在把窗口焦点也算进「离开」:页面失焦即视为后台,失焦瞬间同样补发此前因为「就在你眼前」而被压下的审批提醒;窗口回到前台后照常安静。设置页三个相关开关的文案同步改为「窗口在前台时也提醒」,说明里点明「切走 / 被遮挡 / 最小化」都算后台。
10
+
11
+ ### 测试
12
+
13
+ - 冒烟新增 `awayWithoutHiding` 用例:焦点在时回复完成静默、失焦后同一页发出提醒、回焦再次静默、`visibilityState` 全程保持 `visible`、失焦时补发被压下的审批提醒。
14
+
15
+ ## [1.10.4] — 2026-10-21
16
+
17
+ ### 新增
18
+
19
+ - **桌面端审批按钮落到页内卡片**:桌面壳在 `dsh-app:` 协议下拿不到 Service Worker(注册被拒),带按钮的审批通知因此只剩标题、点不出「同意 / 拒绝」。现在这种拿不到 SW 的情形会在窗口角落画一张卡片:审批标题与正文、`同意` / `拒绝` 两个按钮、正文过长时可就地展开「详情」,12 秒自动收起,同一条审批不叠第二张卡。按钮走的是和系统通知完全同一条裁决路径(`allowed-once` / `rejected`),裁完即撤卡。Web 端(SW 可用)行为不变,仍是带按钮的系统通知。
20
+
21
+ ### 修复
22
+
23
+ - 页面里写的 `api.version` 与 `package.json` 对齐(1.10.3 的发版提交只改了 `package.json`,页内版本号停在 1.10.2,冒烟断言因此一开始就是红的)。
24
+
25
+ ### 测试
26
+
27
+ - 冒烟新增 12 组断言覆盖桌面降级卡片:出现时机、两个按钮、详情展开/收起、点卡片本体不撤卡、同意、拒绝、审批从状态里消失时撤卡、同一条审批不叠卡、开关关闭时不画卡、12 秒回收、Web 路径不回归。
28
+
5
29
  ## [1.10.3] — 2026-10-21
6
30
 
7
31
  ### 修复
package/README.en.md CHANGED
@@ -75,16 +75,16 @@ The Electron application manages the `desktop` profile. Do not change the Web co
75
75
 
76
76
  1. Click once in the DSH page and allow notifications when prompted. If you previously declined, allow them in the browser's site settings. The operating system must also allow notifications from the browser / DSH Desktop.
77
77
  2. Open **Settings → Notify me**. Confirm that the section and plugin version at its foot appear, and leave the master switch, the alert type on system notifications, and sound enabled.
78
- 3. Click **Test "reply finished"** on the settings page. A test notification should appear and sound according to your cue settings. This local test does not forward to external endpoints and ignores the "while the page is open" toggle.
78
+ 3. Click **Test "reply finished"** on the settings page. A test notification should appear and sound according to your cue settings. This local test does not forward to external endpoints and ignores the "while the window is in front" toggle.
79
79
 
80
- Web users can also test approval buttons. On Desktop that test sends a plain notification: **no Approve / Reject buttons is the expected result**.
80
+ Web users can also test approval buttons. On Desktop the shell cannot register a Service Worker, so this test draws an in-page card with Approve / Reject in the window corner (since 1.10.4; earlier versions sent a plain notification).
81
81
 
82
82
  **Optional console check** — in environments with DevTools, you can also run:
83
83
 
84
84
  ```js
85
85
  window.__dshNotifyMe.test("done") // "reply finished" sample
86
86
  window.__dshNotifyMe.test("attention") // "needs your input" sample
87
- window.__dshNotifyMe.test("approval") // Approve / Reject with an active Web bridge; buttonless on Desktop
87
+ window.__dshNotifyMe.test("approval") // Approve / Reject with an active Web bridge; an in-page card on Desktop
88
88
  window.__dshNotifyMe.debug() // the bridge field reports the button channel: "active" = buttons work
89
89
  ```
90
90
 
@@ -108,9 +108,9 @@ dsh-notify-me decouples **supervision** from **sitting in front of the screen**:
108
108
  | 🔔 Input needed | Alerts even in the foreground; the conversation on screen keeps only a title marker |
109
109
  | ✅ Reply finished | Alerts while the page is hidden / backgrounded |
110
110
 
111
- **Input needed** covers approval requests, plan review, and questions (`ask_user_question`). Supported Web environments show Approve / Reject buttons; Desktop does not.
111
+ **Input needed** covers approval requests, plan review, and questions (`ask_user_question`). Approvals carry Approve / Reject: on Web as a system notification with buttons, on Desktop (which cannot register a Service Worker) as a card drawn in the window corner.
112
112
 
113
- **Reply finished** also covers background sessions. A finished but unread session keeps the `✅ Reply finished ·` title marker until opened. Title markers are independent of the "while the page is open" toggles.
113
+ **Reply finished** also covers background sessions. A finished but unread session keeps the `✅ Reply finished ·` title marker until opened. Title markers are independent of the "while the window is in front" toggles.
114
114
 
115
115
  > **Background** subagent child sessions are **not** alerted about by default: DSH lists each of them as its own session, but they are steps inside the parent conversation's turn, so an alert for them is only noise. The subagent session you have open is not part of this and alerts as usual. Settings → "Ignore subagent sessions" turns the mute off.
116
116
 
@@ -125,7 +125,7 @@ Open **Settings → Notify me**. The page runs as how you get told, when to aler
125
125
  - **System notifications** (default): native toasts landing in the Windows / macOS notification center; needs notification permission;
126
126
  - **In-page overlay**: cards drawn in the page corner, on whichever display the window sits on — system notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. With the overlay chosen, a Corner row appears; open it to pick one of four positions. See [Secondary displays and the overlay](#secondary-displays-and-the-overlay);
127
127
  - **Sound / Volume**: the switch is direct; the slider folds into that row — click the title (▸ turns into ▾) to reveal it. What it plays comes from Advanced → Alert sound;
128
- - **Decide approvals from the toast** (on by default): approval toasts carry Approve / Reject that settle the request in place. **Desktop notifications have no buttons** (`dsh-app://` cannot register a Service Worker) — pick the in-page overlay as the channel instead; the page states plainly which channel can carry buttons right now.
128
+ - **Decide approvals from the toast** (on by default): approvals carry Approve / Reject that settle the request in place. On Web that is a system notification with buttons; on Desktop `dsh-app://` cannot register a Service Worker, so the buttons are drawn on a card in the window corner (auto-dismissed after 12 s, one card per approval). You can also switch the channel to the in-page overlay and decide there.
129
129
 
130
130
  ### Secondary displays and the overlay
131
131
 
@@ -143,9 +143,9 @@ The overlay only knows which screen the window is on, not which screen you are l
143
143
 
144
144
  ### When to alert
145
145
 
146
- - **Also alert "needs you" while the page is open** (on): turn it off to hear only from the background;
147
- - **Stay quiet for the conversation on screen** (on): a wait belonging to the conversation you are looking at keeps only the tab marker so the approval card stays readable; backgrounding restores alerts;
148
- - **Also alert "reply finished" while the page is open** (off): by default only background finishes alert;
146
+ - **Also alert "needs you" while the window is in front** (on): turn it off to hear only once the window goes to the background — another app focused, the window covered, or minimized all count;
147
+ - **Stay quiet for the conversation on screen** (on): a wait belonging to the conversation you are looking at keeps only the tab marker so the approval card stays readable; moving the window to the background restores alerts;
148
+ - **Also alert "reply finished" while the window is in front** (off): by default only background finishes alert (a covered Desktop window counts as background since 1.10.5, not only a minimized one);
149
149
  - **Ignore subagent sessions** (on): skip background sessions spawned by a subagent — the parent conversation covers them; the one you have open still alerts;
150
150
  - **Click a toast to open that conversation** (on): the click switches to the conversation that raised it; in a browser it also brings the window forward, while on the Desktop shell it is flaky — sometimes the window comes forward, sometimes only the conversation switches (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)); with several tabs open only one is raised, and off means a click does nothing.
151
151
 
package/README.md CHANGED
@@ -75,16 +75,16 @@ dsh plugin --profile web add dsh-notify-me
75
75
 
76
76
  1. 在 DSH 页面上点击一次,按提示允许通知;若此前拒绝过,在浏览器站点设置中允许通知。操作系统也需要允许对应浏览器 / DSH Desktop 显示通知。
77
77
  2. 打开 **设置 → 通知提醒**,确认出现该分区及底部插件版本号,保持「启用提醒」「提示音」开启,通知类型先留「系统通知」。
78
- 3. 点击设置页的「测试『回复完成』」按钮:应出现一条测试通知,并按当前音效设置播放声音。这个本地测试不会转发到外部通知端点,也不受「页面打开时也提醒」开关限制。
78
+ 3. 点击设置页的「测试『回复完成』」按钮:应出现一条测试通知,并按当前音效设置播放声音。这个本地测试不会转发到外部通知端点,也不受「窗口在前台时也提醒」开关限制。
79
79
 
80
- Web 用户可再测试审批按钮;桌面端该测试只发普通通知,**没有「同意 / 拒绝」按钮属于预期行为**。
80
+ Web 用户可再测试审批按钮。桌面端拿不到 Service Worker,这条测试会在窗口角落画一张带「同意 / 拒绝」的卡片(1.10.4 起;之前的版本这里只发普通通知)。
81
81
 
82
82
  **控制台验证(可选)** — 支持 DevTools 的环境也可执行:
83
83
 
84
84
  ```js
85
85
  window.__dshNotifyMe.test("done") // 「完成」示例
86
86
  window.__dshNotifyMe.test("attention") // 「需要你」示例
87
- window.__dshNotifyMe.test("approval") // Web 桥可用时带「同意 / 拒绝」按钮;桌面端为无按钮通知
87
+ window.__dshNotifyMe.test("approval") // 桥可用时带「同意 / 拒绝」按钮;桌面端画在窗口角落的卡片上
88
88
  window.__dshNotifyMe.debug() // 桥接状态看 bridge 字段,active = 按钮可用
89
89
  ```
90
90
 
@@ -104,9 +104,9 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
104
104
  | 🔔 需要你操作 | 页面可见也提醒;当前对话只留标题标记 |
105
105
  | ✅ 回复完成 | 页面隐藏 / 后台时提醒 |
106
106
 
107
- **需要你操作**包括审批请求、方案待确认(plan review)和提问(`ask_user_question`)。支持快捷裁决的 Web 环境会显示「同意 / 拒绝」按钮,桌面端无按钮。
107
+ **需要你操作**包括审批请求、方案待确认(plan review)和提问(`ask_user_question`)。审批带「同意 / 拒绝」:Web 走带按钮的系统通知,桌面端(注册不了 Service Worker)改画在窗口角落的卡片上。
108
108
 
109
- **回复完成**也覆盖后台会话;完成但还没看时,标题带 `✅ 回复完成 ·` 标记,打开对应会话即撤。标题标记不受「页面打开时也提醒」开关限制。
109
+ **回复完成**也覆盖后台会话;完成但还没看时,标题带 `✅ 回复完成 ·` 标记,打开对应会话即撤。标题标记不受「窗口在前台时也提醒」开关限制。
110
110
 
111
111
  > **后台**子代理(subagent)子会话的等待与完成默认**不提醒**:DSH 把每个子代理子会话列成独立会话,但它们是所属主对话那一轮里的步骤,提醒只会是噪音。你当前打开的那个子代理会话不算在内,照常提醒。设置页「子代理会话不提醒」可关掉这个静音。
112
112
 
@@ -121,7 +121,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
121
121
  - **系统通知**(默认):进 Windows / macOS 通知中心,需要通知权限;
122
122
  - **页面浮层**:在 DSH 页面角落画提醒卡片,窗口在哪块屏它就在哪块屏——系统通知只会画在主屏,主屏不看时用它;不需要通知权限,桌面端也能在卡片上直接「同意 / 拒绝」。选中后卡片里会多出「浮层位置」子行,点开可选四个角落,见[副屏与浮层提醒](#副屏与浮层提醒);
123
123
  - **提示音 / 音量**:开关直接切;音量滑块收在该行里,点一下标题(▸ 变成 ▾)展开。具体播什么由「高级设置 → 音效来源」决定;
124
- - **审批通知上直接裁决**(默认开):审批通知带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。**桌面端的通知没有按钮**(`dsh-app://` 页面无法注册 Service Worker),需要按钮就打开「页面浮层提醒」,改在浮层上裁决;设置页会如实说明当前哪种渠道能给按钮。
124
+ - **审批通知上直接裁决**(默认开):审批带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。Web 走带按钮的系统通知;桌面端 `dsh-app://` 注册不了 Service Worker,于是把按钮画在窗口角落的卡片上(12 秒自动收起,同一条审批不叠第二张卡)。也可以把「通知类型」换成「页面浮层」,改在浮层卡片上裁决。
125
125
 
126
126
  ### 副屏与浮层提醒
127
127
 
@@ -139,9 +139,9 @@ Windows 的系统通知横幅固定画在主屏上,系统没有设置项、浏
139
139
 
140
140
  ### 什么时候提醒
141
141
 
142
- - **页面打开时也提醒「需要你」**(默认开):页面在最前也提醒;关掉后只有切到后台才提醒;
143
- - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;切到后台后照常提醒,若你已处理掉就作废;
144
- - **页面打开时也提醒「回复完成」**(默认关):默认只在切到后台时提醒,避免打扰;
142
+ - **窗口在前台时也提醒「需要你」**(默认开):窗口在前台也提醒;关掉后只有窗口转到后台才提醒——切到别的应用、被别的窗口遮住、最小化都算后台;
143
+ - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;窗口转到后台后照常提醒,若你已处理掉就作废;
144
+ - **窗口在前台时也提醒「回复完成」**(默认关):默认只在窗口转到后台时提醒,避免打扰(桌面端窗口被遮住也算后台,1.10.5 起不再要求最小化);
145
145
  - **子代理会话不提醒**(默认开):跳过后台子代理(subagent)会话——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;
146
146
  - **点击通知回到对应对话**(默认开):点通知切到提醒所属的对话;浏览器里同时把窗口带到前台;桌面端不稳定——有时窗口会上前,有时只切对话(见[桌面端怎么抬窗的](#桌面端怎么抬窗的));多个标签页同时开着时只调起一个(优先当前可见的那个);关掉后点通知不做任何事。
147
147
 
package/lib/client.js CHANGED
@@ -321,13 +321,18 @@
321
321
  // Persist a patch, swap the live module config and re-apply any active
322
322
  // title marker (its prefix follows the notification language).
323
323
  function setConfigLocal(patch) {
324
+ var wasOverlay = config.channel === "overlay";
324
325
  config = writeConfig(patch);
325
326
  applyTitle();
326
- // Overlay re-apply: switching it off takes the cards down at once and a
327
- // new corner moves the layer without waiting for the next alert.
327
+ // Overlay re-apply: leaving the overlay channel takes its cards down at
328
+ // once, and a new corner moves the layer without waiting for the next
329
+ // alert. Cards can also exist on the toast channel (the desktop fallback),
330
+ // so an unrelated patch must not sweep them away.
328
331
  try {
329
- if (config.channel !== "overlay") clearOverlays();
330
- else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
332
+ if (config.channel !== "overlay") {
333
+ if (wasOverlay) clearOverlays();
334
+ else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
335
+ } else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
331
336
  } catch (e) {}
332
337
  return config;
333
338
  }
@@ -384,6 +389,8 @@
384
389
  approve: "同意",
385
390
  reject: "拒绝",
386
391
  dismiss: "关闭这条提醒",
392
+ details: "详情",
393
+ collapse: "收起",
387
394
  testAttention: "这是一条测试提醒:DSH 需要你的操作",
388
395
  testDone: "测试提醒 · 回复完成示例",
389
396
  testApproval: "这是一条测试审批:点「同意 / 拒绝」试试快捷裁决",
@@ -411,6 +418,8 @@
411
418
  approve: "Approve",
412
419
  reject: "Reject",
413
420
  dismiss: "Dismiss this alert",
421
+ details: "Details",
422
+ collapse: "Collapse",
414
423
  testAttention: "This is a test alert: DSH is waiting for your input",
415
424
  testDone: "Test alert · reply-finished sample",
416
425
  testApproval: "This is a test approval: try the Approve / Reject buttons to settle it",
@@ -833,9 +842,11 @@
833
842
  ".dnm-ov-attention .dnm-ov-dot{color:var(--dsw-alias-state-warning-primary,#f59e0b);opacity:1}" +
834
843
  ".dnm-ov-done .dnm-ov-dot{color:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}" +
835
844
  ".dnm-ov-title{flex:1;min-width:0;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
836
- ".dnm-ov-x{flex:none;border:0;background:transparent;color:inherit;opacity:.6;font-size:15px;line-height:15px;padding:0 2px;cursor:pointer;font-family:inherit}" +
837
- ".dnm-ov-x:hover{opacity:1}" +
845
+ ".dnm-ov-x,.dnm-ov-more{flex:none;border:0;background:transparent;color:inherit;opacity:.6;font-size:15px;line-height:15px;padding:0 2px;cursor:pointer;font-family:inherit}" +
846
+ ".dnm-ov-x:hover,.dnm-ov-more:hover{opacity:1}" +
847
+ ".dnm-ov-more{font-size:11px;opacity:.8;white-space:nowrap}" +
838
848
  ".dnm-ov-body{color:var(--dsw-alias-label-secondary,rgba(220,222,226,.86));font-size:12px;line-height:18px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}" +
849
+ ".dnm-ov.dnm-ov-open .dnm-ov-body{display:block;-webkit-line-clamp:unset;max-height:40vh;overflow:auto}" +
839
850
  ".dnm-ov-actions{display:flex;gap:8px;margin-top:2px}" +
840
851
  ".dnm-ov-btn{flex:1;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.06));color:inherit;border-radius:8px;padding:5px 10px;font-size:12px;font-family:inherit;cursor:pointer}" +
841
852
  ".dnm-ov-btn:hover{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.12))}" +
@@ -911,17 +922,30 @@
911
922
  }
912
923
  }
913
924
  function overlayCount() { return Object.keys(overlayCards).length; }
914
- // Same inputs as showToast: `quick` is the approval record built by
915
- // quickFor(), and it is what lets an overlay settle the request in place.
916
- function showOverlay(kind, title, body, sessionId, quick) {
917
- if (config.channel !== "overlay") return false;
925
+ // One card builder for both channels: the overlay channel draws with it,
926
+ // and so does the desktop fallback below. `opts` keeps the two behaviours
927
+ // apart: the overlay card is a shortcut (a click opens the conversation and
928
+ // takes the card down), while the fallback card carries a decision the OS
929
+ // notification cannot — so a click must not dismiss it unanswered, and a
930
+ // "详情 / Details" toggle exposes the body that the CSS clamps to 3 lines.
931
+ function drawOverlayCard(kind, title, body, sessionId, quick, opts) {
932
+ opts = opts || {};
918
933
  var layer = ensureOverlayLayer();
919
934
  if (!layer) return false;
935
+ // One card per approval key: a re-delivery (the quieted copy that fires
936
+ // when the page is backgrounded) must not stack a second card.
937
+ if (quick && typeof quick.key === "string") {
938
+ var known = Object.keys(overlayCards);
939
+ for (var k = 0; k < known.length; k++) {
940
+ if (overlayCards[known[k]].key === quick.key) return false;
941
+ }
942
+ }
920
943
  var d = texts();
921
944
  try {
922
945
  var id = "ov" + (++overlaySeq);
946
+ var baseClass = "dnm-ov dnm-ov-" + (kind === "attention" ? "attention" : "done");
923
947
  var card = document.createElement("div");
924
- card.className = "dnm-ov dnm-ov-" + (kind === "attention" ? "attention" : "done");
948
+ card.className = baseClass;
925
949
  card.setAttribute("role", kind === "attention" ? "alert" : "status");
926
950
  var head = document.createElement("div");
927
951
  head.className = "dnm-ov-head";
@@ -930,6 +954,22 @@
930
954
  var label = document.createElement("span");
931
955
  label.className = "dnm-ov-title";
932
956
  label.textContent = String(title == null ? "" : title);
957
+ var more = null;
958
+ if (opts.expand && body) {
959
+ more = document.createElement("button");
960
+ more.type = "button";
961
+ more.className = "dnm-ov-more";
962
+ more.setAttribute("aria-label", d.details);
963
+ more.setAttribute("aria-expanded", "false");
964
+ more.textContent = d.details;
965
+ more.onclick = function (ev) {
966
+ if (ev && ev.stopPropagation) ev.stopPropagation();
967
+ var open = card.className.indexOf("dnm-ov-open") !== -1;
968
+ card.className = baseClass + (open ? "" : " dnm-ov-open");
969
+ more.textContent = open ? d.details : d.collapse;
970
+ more.setAttribute("aria-expanded", open ? "false" : "true");
971
+ };
972
+ }
933
973
  var close = document.createElement("button");
934
974
  close.type = "button";
935
975
  close.className = "dnm-ov-x";
@@ -941,6 +981,7 @@
941
981
  };
942
982
  head.appendChild(dot);
943
983
  head.appendChild(label);
984
+ if (more) head.appendChild(more);
944
985
  head.appendChild(close);
945
986
  card.appendChild(head);
946
987
  if (body) {
@@ -970,10 +1011,12 @@
970
1011
  }
971
1012
  // A click on the card body opens the conversation, exactly like a toast
972
1013
  // body click — and here it is an ordinary page gesture, so it reaches the
973
- // desktop's dsh:// raise path even though a worker click would not.
1014
+ // desktop's dsh:// raise path even though a worker click would not. The
1015
+ // overlay channel also takes the card down; the fallback card stays, so
1016
+ // the decision is still one click away after a look at the conversation.
974
1017
  card.onclick = function () {
975
1018
  if (config.autoFocus) focusSession(sessionId || null);
976
- dismissOverlay(id);
1019
+ if (opts.dismissOnClick !== false) dismissOverlay(id);
977
1020
  };
978
1021
  layer.appendChild(card);
979
1022
  var rec = { card: card, key: (quick && quick.key) ? quick.key : null, kind: kind, timer: null };
@@ -987,6 +1030,29 @@
987
1030
  return true;
988
1031
  } catch (e) { return false; }
989
1032
  }
1033
+ // Same inputs as showToast: `quick` is the approval record built by
1034
+ // quickFor(), and it is what lets an overlay settle the request in place.
1035
+ function showOverlay(kind, title, body, sessionId, quick) {
1036
+ if (config.channel !== "overlay") return false;
1037
+ return drawOverlayCard(kind, title, body, sessionId, quick, { dismissOnClick: true });
1038
+ }
1039
+ // The desktop shell cannot register the Service Worker that OS action
1040
+ // buttons require, so an approval toast on a dsh-app:// page has no button
1041
+ // to click. While the alert channel is system notifications, an approval
1042
+ // therefore also drops a card into the same in-page layer, in the same
1043
+ // corner and with the same styling: its buttons call decidePending(), the
1044
+ // exact path a Web toast button takes. Only states that prove the bridge is
1045
+ // unusable qualify — 'idle'/'registering' are transitional, 'disabled' means
1046
+ // the quick-decision switch is off (quickFor() then returns null, so no card
1047
+ // is drawn at all), and the Web path stays untouched once the worker is up.
1048
+ function showFallbackApprovalCard(kind, title, body, sessionId, quick) {
1049
+ if (kind !== "attention") return false;
1050
+ if (!quick || typeof quick.key !== "string") return false;
1051
+ if (config.channel !== "toast") return false;
1052
+ if (bridgeActive()) return false;
1053
+ if (swState.indexOf("unsupported:") !== 0 && swState.indexOf("failed:") !== 0) return false;
1054
+ return drawOverlayCard(kind, title, body, sessionId, quick, { dismissOnClick: false, expand: true });
1055
+ }
990
1056
  // Sound: WebAudio beeps (no asset needed). The context only runs after a
991
1057
  // user gesture; we unlock it on the first pointer/key event.
992
1058
  var audioCtx = null;
@@ -1326,6 +1392,25 @@
1326
1392
 
1327
1393
  function hidden() { return document.hidden || document.visibilityState === "hidden"; }
1328
1394
 
1395
+ // "Background" is not the same as "hidden": a desktop window sitting behind
1396
+ // another app, or a browser tab on a second monitor, still reports itself as
1397
+ // visible. Only a minimised or fully occluded window flips document.hidden,
1398
+ // so the reminders that are meant to reach a user who walked away stayed
1399
+ // silent whenever the window was merely covered. Window focus is the missing
1400
+ // half of the signal: an unfocused page counts as away.
1401
+ var pageFocused = true;
1402
+ try {
1403
+ if (typeof document.hasFocus === "function") pageFocused = document.hasFocus() !== false;
1404
+ } catch (e) {}
1405
+ function onPageFocus() { pageFocused = true; }
1406
+ function onPageBlur() {
1407
+ pageFocused = false;
1408
+ // Walking away is what re-arms the waits kept quiet because they belonged
1409
+ // to the conversation on screen; a blur alone never fires visibilitychange.
1410
+ try { flushQuieted(); } catch (e) {}
1411
+ }
1412
+ function away() { return hidden() || !pageFocused; }
1413
+
1329
1414
  // Deliver one alert regardless of visibility rules (used by the test
1330
1415
  // buttons); respects the master switch and permission state. The title
1331
1416
  // marker is NOT touched here — its ownership belongs to the watchers
@@ -1341,6 +1426,7 @@
1341
1426
  if (!config.enabled) return;
1342
1427
  showToast(kind, title, body, sessionId, quick);
1343
1428
  showOverlay(kind, title, body, sessionId, quick);
1429
+ showFallbackApprovalCard(kind, title, body, sessionId, quick);
1344
1430
  if (kind === "attention") {
1345
1431
  playAttentionSound();
1346
1432
  } else {
@@ -1350,13 +1436,13 @@
1350
1436
  }
1351
1437
  function fire(kind, title, body, isCurrent, sessionId, quick) {
1352
1438
  if (!config.enabled) return false;
1353
- if (kind === "attention" && config.attentionHiddenOnly && !hidden()) return false;
1439
+ if (kind === "attention" && config.attentionHiddenOnly && !away()) return false;
1354
1440
  // The wait belongs to the conversation the user is looking at right now:
1355
1441
  // the approval card is already on screen, so a toast would only cover the
1356
1442
  // thing it is asking about. The caller keeps the tab marker, and
1357
1443
  // flushQuieted() re-delivers the alert the moment the page is backgrounded.
1358
1444
  if (kind === "attention" && quietedByVisibleSession(isCurrent)) return false;
1359
- if (kind === "done" && config.doneHiddenOnly && !hidden()) return false;
1445
+ if (kind === "done" && config.doneHiddenOnly && !away()) return false;
1360
1446
  deliver(kind, title, body, sessionId, quick);
1361
1447
  // The alert reached the user locally (or would have, had the tab a
1362
1448
  // voice): mirror the same copy to the outside world. Fire-and-forget —
@@ -1368,7 +1454,7 @@
1368
1454
  // session already on screen. Kept separate from fire() so the caller can
1369
1455
  // queue exactly this case and nothing else.
1370
1456
  function quietedByVisibleSession(isCurrent) {
1371
- return !!(isCurrent && config.currentHiddenOnly && !hidden());
1457
+ return !!(isCurrent && config.currentHiddenOnly && !away());
1372
1458
  }
1373
1459
  // Queued copies of the alerts that rule suppressed. Delivered by key so a
1374
1460
  // wait that got answered while the user was still on the page never fires.
@@ -1376,7 +1462,7 @@
1376
1462
  if (key && copy) quietedAttention[key] = copy;
1377
1463
  }
1378
1464
  function flushQuieted() {
1379
- if (!config.enabled || !hidden()) return;
1465
+ if (!config.enabled || !away()) return;
1380
1466
  var keys = Object.keys(quietedAttention);
1381
1467
  for (var i = 0; i < keys.length; i++) {
1382
1468
  var copy = quietedAttention[keys[i]];
@@ -1980,7 +2066,7 @@
1980
2066
  });
1981
2067
  } catch (e) {}
1982
2068
  // keep in sync with package.json "version" on every release
1983
- api.version = "1.10.2";
2069
+ api.version = "1.10.5";
1984
2070
  api.setConfig = function (patch) { return setConfigLocal(patch); };
1985
2071
  api.resetConfig = function () {
1986
2072
  try {
@@ -2082,6 +2168,8 @@
2082
2168
  actionKeys: Object.keys(pendingActions),
2083
2169
  permission: ("Notification" in window) ? Notification.permission : "n/a",
2084
2170
  visibility: hidden() ? "hidden" : "visible",
2171
+ focused: pageFocused,
2172
+ away: away(),
2085
2173
  uiSession: uiPending ? "bound" : "unbound",
2086
2174
  uiSessionNote: uiBindNote,
2087
2175
  uiSessionSource: uiSource,
@@ -2182,27 +2270,27 @@
2182
2270
  externalCountFailed: "失败 ",
2183
2271
  externalNoTargets: "先添加至少一个推送目标。",
2184
2272
  externalOffHint: "先打开「转发外部通知」。",
2185
- attentionVisible: "页面打开时也提醒「需要你」",
2186
- attentionVisibleHint: "页面在最前也提醒。关掉后只有切到后台才提醒。",
2273
+ attentionVisible: "窗口在前台时也提醒「需要你」",
2274
+ attentionVisibleHint: "窗口在前台也提醒。关掉后只在窗口转到后台(切走、被遮挡或最小化)时提醒。",
2187
2275
  currentQuiet: "当前对话不弹通知",
2188
- currentQuietHint: "等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;切到后台后照常提醒。",
2189
- doneVisible: "页面打开时也提醒「回复完成」",
2190
- doneVisibleHint: "页面在最前也提醒。默认只在切到后台时提醒,避免打扰。",
2276
+ currentQuietHint: "等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;窗口转到后台后照常提醒。",
2277
+ doneVisible: "窗口在前台时也提醒「回复完成」",
2278
+ doneVisibleHint: "窗口在前台也提醒。默认只在窗口转到后台时提醒,避免打扰。",
2191
2279
  subagentQuiet: "子代理会话不提醒",
2192
2280
  subagentQuietHint: "跳过后台子代理会话(结果由所属主对话的提醒覆盖);你正打开的那个照常提醒。",
2193
2281
  autoFocus: "点击通知回到对应对话",
2194
2282
  autoFocusHint: "点通知会切到这条提醒所属的对话;关掉后点击不做任何事。",
2195
2283
  quickActions: "审批通知上直接裁决",
2196
- quickActionsHint: "审批通知上带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。需要 Service Worker(http://127.0.0.1 或 https),用不了时退回普通通知。",
2284
+ quickActionsHint: "审批通知上带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。需要 Service Worker(http://127.0.0.1 或 https);用不了时按钮改画在窗口角落的卡片上(桌面端就是这条路)。",
2197
2285
  quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
2198
2286
  bridgeOff: "快捷裁决按钮暂不可用(",
2199
- bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
2200
- bridgeOffDesktop: "系统通知没有「同意 / 拒绝」按钮:桌面端页面(dsh-app://)注册不了 Service Worker(宿主少给 allowServiceWorkers 特权),按钮发不出来。想一键裁决就把「提醒方式」切到「页面浮层」。",
2287
+ bridgeOffTail: "):系统通知没有按钮,按钮改画在窗口角落的卡片上。",
2288
+ bridgeOffDesktop: "系统通知本身没有「同意 / 拒绝」按钮:桌面端页面(dsh-app://)注册不了 Service Worker(宿主少给 allowServiceWorkers 特权)。审批类提醒会同时在窗口角落弹一张卡片,按钮就在卡片上;想只留卡片、不弹系统通知,把「提醒方式」切到「页面浮层」。",
2201
2289
  overlay: "页面浮层提醒",
2202
2290
  overlayHint: "在窗口角落画提醒卡片:窗口在哪块屏,它就在哪块屏;系统通知只会出现在主屏,主屏不看时用它。不需要通知权限,卡片上能直接「同意 / 拒绝」。与系统通知二选一。",
2203
2291
  channelLabel: "通知类型",
2204
2292
  overlaySeg: "浮层提醒",
2205
- channelHint: "系统通知只会出现在主屏,进通知中心要权限,桌面端也没有「同意 / 拒绝」按钮;页面浮层画在 DSH 窗口所在的那块屏,不需要权限,卡片上能直接裁决。两个只能选一个。",
2293
+ channelHint: "系统通知只会出现在主屏,进通知中心要权限,桌面端的通知本身没有「同意 / 拒绝」按钮(审批会额外在窗口角落弹一张可点击的卡片);页面浮层画在 DSH 窗口所在的那块屏,不需要权限,卡片上能直接裁决。两个只能选一个。",
2206
2294
  overlayCorner: "浮层位置",
2207
2295
  overlayCornerHint: "贴在 DSH 窗口的哪个角;窗口在哪块屏,浮层就出现在哪块屏。",
2208
2296
  overlayPosTR: "右上",
@@ -2210,7 +2298,7 @@
2210
2298
  overlayPosTL: "左上",
2211
2299
  overlayPosBL: "左下",
2212
2300
  overlayQuickNote: "浮层上的「同意」同样等价于审批卡片上的「允许一次」,与通知按钮一致。",
2213
- testApprovalNoBridge: "已发普通测试通知(快捷裁决按钮需要 Service Worker,当前环境没有,按钮渲染不了)。",
2301
+ testApprovalNoBridge: "已发测试提醒:系统通知没有按钮,快捷裁决按钮画在窗口角落的卡片上。",
2214
2302
  testNeedsQuick: "先打开上面的「审批通知上直接裁决」才能测按钮。",
2215
2303
  volume: "音量",
2216
2304
  language: "通知语言",
@@ -2312,27 +2400,27 @@
2312
2400
  externalCountFailed: "failed ",
2313
2401
  externalNoTargets: "Add at least one push target first.",
2314
2402
  externalOffHint: "Turn on \"Forward to external targets\" first.",
2315
- attentionVisible: "Also alert \"needs you\" while the page is open",
2316
- attentionVisibleHint: "Alert even while the page is in front. Turn off to hear only from the background.",
2403
+ attentionVisible: "Also alert \"needs you\" while the window is in front",
2404
+ attentionVisibleHint: "Alert even while the window has focus. Turn off to hear only once the window goes to the background (another app, covered, or minimised).",
2317
2405
  currentQuiet: "Stay quiet for the conversation on screen",
2318
- currentQuietHint: "A wait belonging to the conversation on screen keeps only the tab marker, so the approval card stays readable; backgrounding restores alerts.",
2319
- doneVisible: "Also alert \"reply finished\" while the page is open",
2320
- doneVisibleHint: "Alert even while the page is in front. By default only background finishes alert.",
2406
+ currentQuietHint: "A wait belonging to the conversation on screen keeps only the tab marker, so the approval card stays readable; moving the window to the background restores alerts.",
2407
+ doneVisible: "Also alert \"reply finished\" while the window is in front",
2408
+ doneVisibleHint: "Alert even while the window has focus. By default only finishes in the background alert.",
2321
2409
  subagentQuiet: "Ignore subagent sessions",
2322
2410
  subagentQuietHint: "Skip sessions spawned by a subagent (the parent conversation covers them); the one you have open still alerts.",
2323
2411
  autoFocus: "Click a toast to open that conversation",
2324
2412
  autoFocusHint: "A click switches to the conversation the alert came from; off means a click does nothing.",
2325
2413
  quickActions: "Decide approvals from the toast",
2326
- quickActionsHint: "Approval notifications carry Approve / Reject; one click settles the request without switching back. Needs a Service Worker browser (http://127.0.0.1 or https), else plain toasts.",
2414
+ quickActionsHint: "Approval notifications carry Approve / Reject; one click settles the request without switching back. Needs a Service Worker browser (http://127.0.0.1 or https); when it is unavailable the buttons land on a card in the window corner (the Desktop shell's route).",
2327
2415
  quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
2328
2416
  bridgeOff: "Quick-decision buttons are unavailable right now (",
2329
- bridgeOffTail: "); approval toasts fall back to their plain form.",
2330
- bridgeOffDesktop: "System notifications carry no Approve / Reject buttons: dsh-app:// pages cannot register a Service Worker (the shell does not grant the allowServiceWorkers privilege), so the buttons cannot be attached. Switch the alert channel to the in-page overlay to settle approvals in one click.",
2417
+ bridgeOffTail: "); system notifications carry no buttons, so they are drawn on a card in the window corner instead.",
2418
+ bridgeOffDesktop: "System notifications carry no Approve / Reject buttons: dsh-app:// pages cannot register a Service Worker (the shell does not grant the allowServiceWorkers privilege). An approval therefore also drops a clickable card in the window corner, buttons and all; switch the alert channel to the in-page overlay to keep only the card and skip the system notification.",
2331
2419
  overlay: "In-page overlay",
2332
2420
  overlayHint: "Draw a card in the window corner, on whatever display the window sits on. System notifications only ever show on the primary display. No permission needed, and the card carries Approve / Reject directly. It replaces system notifications rather than adding to them.",
2333
2421
  channelLabel: "Alert type",
2334
2422
  overlaySeg: "Overlay",
2335
- channelHint: "System notifications land in the Windows notification center: permission needed, primary display only, and no Approve / Reject buttons on the Desktop shell. The in-page overlay draws on whichever display the DSH window sits on, needs no permission, and lets the Desktop shell decide right on the card. Pick one.",
2423
+ channelHint: "System notifications land in the Windows notification center: permission needed, primary display only, and the Desktop shell's notifications carry no Approve / Reject buttons (an approval also drops a clickable card in the window corner). The in-page overlay draws on whichever display the DSH window sits on, needs no permission, and lets the Desktop shell decide right on the card. Pick one.",
2336
2424
  overlayCorner: "Overlay position",
2337
2425
  overlayCornerHint: "Which corner of the DSH window the cards stack in; they follow the window to whatever display it is on.",
2338
2426
  overlayPosTR: "Top right",
@@ -2340,7 +2428,7 @@
2340
2428
  overlayPosTL: "Top left",
2341
2429
  overlayPosBL: "Bottom left",
2342
2430
  overlayQuickNote: "\"Approve\" here equals \"Allow once\" on the approval card — the same decision the toast buttons send.",
2343
- testApprovalNoBridge: "Plain test notification sent (quick-decision buttons need a Service Worker, which this environment lacks, so they cannot render).",
2431
+ testApprovalNoBridge: "Test alert sent: system notifications carry no buttons, so the quick-decision buttons are drawn on a card in the window corner.",
2344
2432
  testNeedsQuick: "Turn on \"Decide approvals from the toast\" above to test the buttons.",
2345
2433
  volume: "Volume",
2346
2434
  language: "Notification language",
@@ -3314,6 +3402,13 @@
3314
3402
  var onVisibility = function () { try { flushQuieted(); } catch (e) {} };
3315
3403
  try { document.addEventListener("visibilitychange", onVisibility); } catch (e) {}
3316
3404
  unsubs.push(function () { try { document.removeEventListener("visibilitychange", onVisibility); } catch (e) {} });
3405
+ // Same re-arm for the other half of "went away": losing window focus.
3406
+ try { window.addEventListener("focus", onPageFocus); } catch (e) {}
3407
+ try { window.addEventListener("blur", onPageBlur); } catch (e) {}
3408
+ unsubs.push(function () {
3409
+ try { window.removeEventListener("focus", onPageFocus); } catch (e) {}
3410
+ try { window.removeEventListener("blur", onPageBlur); } catch (e) {}
3411
+ });
3317
3412
  // Quick-decision bridge: register the worker half of this bundle (a
3318
3413
  // no-op where quickActions is off or a worker cannot run) and start
3319
3414
  // listening for toast-button relays.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-notify-me",
3
- "version": "1.10.3",
4
- "description": "离开 DSH 页面也能收到提醒:需要你操作 / 后台回复完成时发系统通知、标题标记与可选页面浮层(副屏也能看见),支持审批快捷裁决与外部推送。Brand icon and a reworked settings page in 1.9.0.",
3
+ "version": "1.10.5",
4
+ "description": "离开 DSH 页面也能收到提醒:需要你操作 / 后台回复完成时发系统通知、标题标记与可选页面浮层(副屏也能看见),支持审批快捷裁决与外部推送,Web 版与桌面版均可用。",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",