dsh-notify-me 1.5.1 → 1.5.3

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.5.3] — 2026-10-04
6
+
7
+ ### 新增
8
+ - **设置页底部常显插件版本号**(`dsh-notify-me v…`,中英跟随界面语言):报障、核对是否装上新版本时直接照抄这一行,不必再开控制台查 `window.__dshNotifyMe.version`(该字段保留)。
9
+
10
+ ### 变更
11
+ - 版本号 1.5.2 → 1.5.3;`window.__dshNotifyMe.version` 同步。
12
+ - README(中英):「桌面端怎么抬窗的 / How the Desktop window gets raised」补真实壳复测结论与触发时机口径——深链必须在带用户手势的处理器里发(通知 `onclick` 自带手势,产品路径通);无手势的脚本化注入会被 Chromium 静默丢弃、外部协议不回流,复现时看似「通道被拦死」。致谢名单新增 [@EliteOtaku](https://github.com/EliteOtaku)(issue #9 的逐层排查与三轮真实壳复测)。
13
+
14
+ ### 核实
15
+ - issue #9 真实壳三轮复测(DSH Desktop 0.2.0-rc.2 / Windows 11):点通知 = 抬窗 + 切会话一步到位,同会话、跨会话、窗口在后台均稳定 ✅,1.5.2 的抬窗修复确认生效。
16
+ - 修正 1.5.2 发布当天「CDP 注入 iframe 导航 0 新进程 ⇒ 沙箱 renderer 拦截外部协议」的判断:从发行包 `lib/main.js` 逐处核对,壳对主窗口既没有 `will-frame-navigate` 拦截、页面无 CSP,非 media 权限(含 `openExternal`)一律放行;真实差异在**用户手势上下文**,与渲染进程的 OS 沙箱无关(OS 沙箱 ≠ iframe 的 `sandbox` 属性,GHSA-p2rr-rvmm-c5fp 只影响后者)。
17
+
18
+ ## [1.5.2] — 2026-10-04
19
+
20
+ ### 新增
21
+ - **设置页新增「运行环境」**(默认自动识别):按页面协议(`dsh-app://` = 桌面端 DSH Desktop)识别 Web 端 / 桌面端,识别不准时可手动指定「桌面端」或「Web 端」。手动覆盖同时决定两件事:点通知的抬窗通道(桌面端 `dsh://` 深链 / Web 端 `window.focus()`)与快捷裁决按钮的可用性标注;覆盖与自动识别不一致时设置页明示检测结果,强选「桌面端」但页面并非 `dsh-app://` 时另提示可能弹「打开应用」确认框。
22
+
23
+ ### 修复
24
+ - **桌面端「点击通知回到对应对话」真正把窗口带到前台**(issue #9):DSH Desktop 的页面跑在 `dsh-app://app`(Electron),DOM `window.focus()` 碰不到原生窗口,此前点通知只在后台切好对话、窗口不上前台。现在点通知正文时页面发一条隐藏 iframe 导航到 `dsh://open` —— 子框架导航不经过壳的 `will-navigate` / window-open 拦截,URL 交给系统协议注册,应用单实例锁的 `second-instance` 处理走壳自己的 `focusPrimaryWindow()`(restore + show + focus)。窗口隐藏在托盘、被别的窗口压住、最小化都能抬回来;切对话行为不变,浏览器端不受影响。
25
+
26
+ ### 变更
27
+ - **设置页在桌面端明示快捷裁决按钮不可用**:`dsh-app://` 页面无法注册 Service Worker(实测),审批通知的「同意 / 拒绝」按钮渲染不了、自动退回无按钮样式;此前开关本身毫无标注(只有测试按钮的反馈里带一行原始 `bridge` 状态),容易被当成开关坏了。现在设置页在桌面端直接说明这是宿主限制;「测试『审批按钮』」在无桥环境发普通测试通知时也明说按钮渲染不了。
28
+ - **测试按钮的反馈文字移到按钮正下方**:此前落在页面最底部(常在视口外),点了测试看不到任何反馈,像「没反应」;无桥 / 开关关闭时的提示也一并放在同一位置。
29
+ - `window.__dshNotifyMe.debug()` 新增 `desktop`(生效环境是否桌面端)、`raiseChannel`(`dsh-deeplink` / `window-focus`)、`hostEnv`(设置值)与 `hostEnvDetected`(自动识别结果)。
30
+ - 版本号 1.5.1 → 1.5.2;`window.__dshNotifyMe.version` 同步。
31
+ - README(中英)同步:设置清单加「运行环境」,新增「桌面端怎么抬窗的 / How the Desktop window gets raised」一节,「点击通知回到对应对话」「审批通知上直接裁决」两条口径与已知限制(桌面端按钮不可用由「未实测」改为实测结论)。
32
+
33
+ ### 测试
34
+ - 抬窗通道实测:最小 Electron 44 复刻(`setWindowOpenHandler` / `will-navigate` 与桌面壳同款拦截、不装 `setPermissionRequestHandler`)上,非沙箱 iframe 导航 `dsh://open` 抬起真实 DSH Desktop 窗口(restore + 前台约 0.8s),沙箱 iframe 被拒(Electron GHSA-p2rr-rvmm-c5fp 修复生效),`window.focus()` 不抬(与 issue #9 结论一致);`dsh://open` 经系统协议回流四轮实测均可靠抬窗。
35
+ - `smoke/smoke-test.cjs` 新增桌面抬窗通道用例段:桌面端点通知正文发 `dsh://open` 深链且仍切对话、浏览器端绝不发深链、`hostEnv` 手动覆盖双向生效(强 Web 桌面页不发链、强桌面 Web 页发链)。
36
+
5
37
  ## [1.5.1] — 2026-10-04
6
38
 
7
39
  ### 修复
package/README.en.md CHANGED
@@ -52,14 +52,16 @@ dsh-notify-me decouples **supervision** from **sitting in front of the screen**:
52
52
  Open **Settings → Notify me** (refresh the page once after installing):
53
53
 
54
54
  - **Enable reminders** — master switch; when off, no toasts, no sounds and the tab title is never touched;
55
+ - **Environment** (auto-detect by default) — identifies Web vs Desktop (DSH Desktop, `dsh-app://`) from the page protocol, with a manual override for hosts where detection comes out wrong: on the Desktop a toast click rides the `dsh://` deep link to raise the window and quick-decision buttons are unavailable; on the Web it is `window.focus()` plus the Service Worker bridge. Whenever the override disagrees with detection, the settings page says what detection saw;
55
56
  - **System notifications / Sound / Volume** — toast toggle, WebAudio beep toggle and a volume slider;
56
57
  - **"Needs you" alerts while the page is open** (default on) and **"Reply finished" alerts while the page is open** (default off);
57
58
  - **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
59
  - **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. 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
- - **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;
60
+ - **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. On the Desktop (DSH Desktop) the click likewise raises the window and switches conversations: page-side `window.focus()` cannot raise a native Electron window, so since 1.5.2 the plugin rides the app's own `dsh://open` deep link instead (a window hidden to the tray comes back too);
61
+ - **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. **Buttons are unavailable on the Desktop** (`dsh-app://` pages cannot register a Service Worker): approval toasts fall back to their buttonless form there, and the settings page says so;
61
62
  - **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
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).
63
+ - **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);
64
+ - **Version info** — the settings page always shows the installed plugin version at its foot (`dsh-notify-me v…`); quote that line in bug reports or to check which build you are on.
63
65
 
64
66
  > Notification permission is required: click once on the page → **Allow** (or address-bar lock → Site settings → Notifications → Allow → reload). On macOS, also allow the browser under System Settings → Notifications.
65
67
 
@@ -147,14 +149,20 @@ Action buttons (`actions`) belong to persistent notifications shown through `Ser
147
149
 
148
150
  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
151
 
152
+ ### How the Desktop window gets raised
153
+
154
+ DSH Desktop is a thin Electron shell and its page lives under `dsh-app://app`: DOM `window.focus()` only touches the browsing-context window, never the native one, and the shell's `will-navigate` / window-open handlers block every ordinary way for the page to fire a `dsh://` URL — so through 1.5.1 a toast click switched the conversation in the background but never brought the window forward. Since 1.5.2 a body click navigates a hidden iframe to `dsh://open`: subframe navigations pass the shell's `will-navigate` interception, the URL reaches the OS protocol registration, and the app's single-instance owner runs its own `focusPrimaryWindow()` (restore + show + focus) — the same path the login completion page uses to bring the client forward. A window hidden to the tray, buried under others, or minimized all come back. Browsers are unaffected and still use `window.focus()` / the worker's `client.focus()`.
155
+
156
+ Verified on the real shell (DSH Desktop 0.2.0-rc.2, three rounds of retesting in issue #9): a toast click raises the window and switches the conversation in one step, stable across sessions and with the window in the background. Timing of the fire is what makes the chain work: the deep link must be launched from a **user-gesture handler** — the notification's `onclick` carries one, which is why the product path works — while gesture-less scripted injection (an iframe appended from a CDP evaluate, say) is silently dropped by Chromium and the external protocol never launches, which looks exactly like the channel being blocked.
157
+
150
158
  ## Known limitations
151
159
 
152
160
  - The page must be open for alerts to fire (background tab / minimized is fine; closing the tab stops it — that's inherent to a browser-layer plugin).
153
161
  - Notifications appear through the browser, so the browser needs "show notifications" permission in the OS notification settings, and notification-center "Do Not Disturb" must not suppress them.
154
162
  - The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
155
163
  - Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
156
- - Quick-decision buttons need a Service Worker: they work on `http://127.0.0.1` and `https`, not on plain `http://192.168.x.x`-style LAN addresses, and some browsers or desktop hosts never render notification buttons. The `bridge` field of `window.__dshNotifyMe.debug()` says why; toasts then fall back to their buttonless form and everything else keeps working.
157
- - macOS: in the browser, alerts, sounds, the title marker and toast-click navigation carry no OS assumptions and work as on Windows. Two caveats: DSH Desktop (Electron) notifications require the app to be code-signed — unsigned builds simply emit no notification — and whether the official macOS build satisfies that is untested; and notification buttons on macOS / desktop hosts are likewise unrendered in some cases, falling back to the buttonless form above.
164
+ - Quick-decision buttons need a Service Worker: they work on `http://127.0.0.1` and `https`, not on plain `http://192.168.x.x`-style LAN addresses, and some browsers or desktop hosts never render notification buttons. **The Desktop (`dsh-app://`) is tested and cannot register a Service Worker**, so buttons never render there and approval toasts fall back to their buttonless form. The `bridge` field of `window.__dshNotifyMe.debug()` says why; everything else keeps working.
165
+ - macOS: in the browser, alerts, sounds, the title marker and toast-click navigation carry no OS assumptions and work as on Windows. Two caveats: DSH Desktop (Electron) notifications require the app to be code-signed — unsigned builds simply emit no notification — and whether the official macOS build satisfies that is untested; and notification buttons on the macOS desktop host are likewise untested (the Windows desktop host is tested and renders none, see the line above), falling back to the buttonless form above.
158
166
  - Config lives in browser `localStorage`: switching browsers/devices or clearing site data returns to defaults (one-click restore in the Settings page).
159
167
  - 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.
160
168
  - 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.
@@ -174,10 +182,11 @@ npm pack --dry-run # preview the published tarball
174
182
 
175
183
  ## Credits & how contributions land
176
184
 
177
- Contributors (by handle; the linked PR is their change):
185
+ Contributors (by handle; the link points at the PR / issue they worked on):
178
186
 
179
187
  - [@AgMahone](https://github.com/AgMahone) — [#7](https://github.com/chromoany/dsh-notify-me/pull/7): the 0.1.6+ / 0.2.0 host-generation support, plus the double-chime and cordis-lookup reports and retesting;
180
- - [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8): muting subagent sessions (`ignoreSubagent`) with its tests and docs; the 1.4.0 refinement — mute only background subagents, alert as usual for the one you have open — is his suggestion from the #8 discussion, landed by the maintainer.
188
+ - [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8): muting subagent sessions (`ignoreSubagent`) with its tests and docs; the 1.4.0 refinement — mute only background subagents, alert as usual for the one you have open — is his suggestion from the #8 discussion, landed by the maintainer;
189
+ - [@EliteOtaku](https://github.com/EliteOtaku) — [#9](https://github.com/chromoany/dsh-notify-me/issues/9): the layer-by-layer investigation and three rounds of real-shell retesting of the desktop raise path (1.5.2) — confirming that a toast click raises the window and switches conversations reliably, and pinning down the reproduction difference ("gesture-less scripted injection is silently dropped by Chromium"), which corrected the initial "the sandbox blocks it" reading.
181
190
 
182
191
  Attribution policy: external PRs are landed as maintainer commits — the original commit is not reused and no Co-authored-by trailer is added, so GitHub's contributors graph lists the maintainer only and contributors are credited above.
183
192
 
package/README.md CHANGED
@@ -52,14 +52,16 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
52
52
  打开 **设置 → 通知提醒**(首次安装后刷新一次页面即可看到),页面提供:
53
53
 
54
54
  - **启用提醒** 主开关:关闭后不再弹系统通知、不播放提示音、也不改标签页标题;
55
+ - **运行环境**(默认自动识别):按页面协议识别 Web 端 / 桌面端(DSH Desktop,`dsh-app://`),识别不准时可手动指定——桌面端点通知走 `dsh://` 深链抬窗、快捷裁决按钮不可用;Web 端走 `window.focus()` 与 Service Worker 桥。手动覆盖与自动识别不一致时,设置页会明示检测结果;
55
56
  - **系统通知 / 提示音 / 音量**:Toast、WebAudio 提示音与音量滑块;
56
57
  - **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
57
58
  - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标题标记,不弹通知也不响音(免得盖住审批卡片);页面切到后台立刻补发,若你已经处理掉就作废。其它后台会话不受影响;
58
59
  - **子代理会话不提醒**(默认开):后台子代理(subagent)子会话的「需要你」与「回复完成」都不提醒——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;关掉后恢复逐会话提醒;
59
- - **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;多个 DSH 标签页同时开着时,一次点击只调起一个窗口(优先当前可见的那个)并只切它的对话,其余标签页不动;关掉后点通知不做任何事;
60
- - **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」;
60
+ - **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;多个 DSH 标签页同时开着时,一次点击只调起一个窗口(优先当前可见的那个)并只切它的对话,其余标签页不动;关掉后点通知不做任何事。桌面端(DSH Desktop)同样会把窗口带到前台并切对话:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.5.2 起改走应用自己的 `dsh://open` 深链调窗(窗口隐藏在托盘里也能被抬回来);
61
+ - **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」。**桌面端暂不支持按钮**(`dsh-app://` 页面无法注册 Service Worker),审批通知自动退回无按钮样式,设置页会明示这一限制;
61
62
  - **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
62
- - **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
63
+ - **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失);
64
+ - **版本信息**:设置页底部常显当前插件版本号(`dsh-notify-me v…`)——报障、核对是否装上新版本时直接照抄这一行。
63
65
 
64
66
  > 需要浏览器通知权限:页面上点一下 → 允许;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新。macOS 还要在 系统设置 → 通知 里允许该浏览器。
65
67
 
@@ -143,14 +145,20 @@ window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:
143
145
 
144
146
  每条按钮通知按交互 key 记账,一次只认领一条等待;请求被页面里答掉、或被新请求顶替之后,残留通知上的按钮再点也不会误裁,只是安静地关掉。带按钮的通知不设自动消失——审批没裁决就一直留在通知中心里等你。
145
147
 
148
+ ### 桌面端怎么抬窗的
149
+
150
+ DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的 `window.focus()` 只动浏览器窗口对象、碰不到原生窗口,壳的 `will-navigate` / `window-open` 又把页面里发 `dsh://` 的常规出口全堵上了——所以 1.5.1 及以前点通知只在后台切好对话,窗口不上前台。1.5.2 起点通知正文时页面发一条隐藏 iframe 导航到 `dsh://open`:子框架导航不经过壳的 `will-navigate` 拦截,URL 交给系统协议注册,应用的单实例锁收到第二次启动后走 `focusPrimaryWindow()`(restore + show + focus)——与登录完成页把客户端置前是同一条路。窗口隐藏在托盘、被别的窗口压住、最小化,都能被抬回来。浏览器端不受影响,仍走 `window.focus()` / worker `client.focus()`。
151
+
152
+ 真实壳已实测(DSH Desktop 0.2.0-rc.2,issue #9 三轮复测):点通知 = 抬窗 + 切会话一步到位,跨会话、窗口在后台时同样稳定。触发时机是这条链路的关键:深链必须在**带用户手势的处理器**里发——通知的 `onclick` 自带手势,所以产品路径通;而无手势的脚本化注入(如 CDP evaluate 手工塞 iframe)会被 Chromium 静默丢弃、外部协议根本不回流,复现时看起来就像「通道被拦死」。
153
+
146
154
  ## 已知限制
147
155
 
148
156
  - 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
149
157
  - 通知经由浏览器弹出,需允许浏览器通知权限,且勿扰模式不能屏蔽它。
150
158
  - 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
151
159
  - 未授权通知权限时只有提示音与标题标记。
152
- - 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;浏览器或桌面宿主不渲染通知按钮时,`window.__dshNotifyMe.debug()` 的 `bridge` 字段会说明原因,通知自动退回无按钮样式,其余功能不受影响。
153
- - macOS:浏览器里提醒、提示音、标题标记、点通知跳转都没有平台假设,照常工作;但 DSH Desktop(Electron)的系统通知要求应用已签名,未签名的通知直接发不出去,官方 macOS 版是否满足这一前提未实测;审批按钮在 macOS 与桌面宿主上的渲染同样未实测,不渲染时自动退回无按钮通知。
160
+ - 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;**桌面端(`dsh-app://`)已实测无法注册 Service Worker**,按钮渲染不了,审批通知退回无按钮样式。浏览器或桌面宿主不渲染通知按钮时,`window.__dshNotifyMe.debug()` 的 `bridge` 字段会说明原因,其余功能不受影响。
161
+ - macOS:浏览器里提醒、提示音、标题标记、点通知跳转都没有平台假设,照常工作;但 DSH Desktop(Electron)的系统通知要求应用已签名,未签名的通知直接发不出去,官方 macOS 版是否满足这一前提未实测;审批按钮在 macOS 桌面宿主上的渲染同样未实测(Windows 桌面端已实测不渲染,见上条),不渲染时自动退回无按钮通知。
154
162
  - 配置存在浏览器 `localStorage`:换浏览器/设备或清除站点数据后会回到默认值(设置页可一键恢复默认)。
155
163
  - `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文只有会话名,不再附带回复摘要——宿主快照已不再提供 `nodes`;提醒本身照常触发。
156
164
  - 标题标记由宿主与插件共同写 `document.title`:宿主重算标题时标记可能被覆盖,直到下一次提醒事件重新写成。
@@ -170,10 +178,11 @@ npm pack --dry-run # 预览发布包
170
178
 
171
179
  ## 致谢与贡献须知
172
180
 
173
- 贡献者(按 handle 列出,对应 PR 就是他们提的改动):
181
+ 贡献者(按 handle 列出,链接指向他们参与的 PR / issue):
174
182
 
175
183
  - [@AgMahone](https://github.com/AgMahone) — [#7](https://github.com/chromoany/dsh-notify-me/pull/7):0.1.6+ / 0.2.0 宿主代际支持,以及通知双响、cordis 查找路径的报障与复测;
176
- - [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8):子代理会话静音(`ignoreSubagent`)与配套测试、文档;1.4.0 里「只静音后台子代理、当前打开的照常提醒」的口径同样出自他在 #8 讨论里的建议,落地由维护者完成。
184
+ - [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8):子代理会话静音(`ignoreSubagent`)与配套测试、文档;1.4.0 里「只静音后台子代理、当前打开的照常提醒」的口径同样出自他在 #8 讨论里的建议,落地由维护者完成;
185
+ - [@EliteOtaku](https://github.com/EliteOtaku) — [#9](https://github.com/chromoany/dsh-notify-me/issues/9):桌面端抬窗链路(1.5.2)的逐层排查与真实壳三轮复测——确认点通知抬窗 + 切会话稳定生效,并定位出「无用户手势的脚本化注入被 Chromium 静默丢弃」这一复现差异,纠正了最初「沙箱拦死」的判断。
177
186
 
178
187
  署名口径:本仓库收编外部 PR 时以维护者提交落地——不沿用原 commit、不挂 Co-authored-by,GitHub 的 contributors 图只显示维护者,贡献者以上面的致谢署名。
179
188
 
package/lib/client.js CHANGED
@@ -216,6 +216,12 @@
216
216
  // language of the notification copy: 'auto' follows the DSH UI locale,
217
217
  // 'zh' and 'en' pin Simplified Chinese / English.
218
218
  language: "auto",
219
+ // Which host environment this page runs in: 'auto' detects from the page
220
+ // protocol (dsh-app:// = the packaged DSH Desktop shell), 'desktop' and
221
+ // 'web' force it. The desktop rides the dsh:// deep-link raise path and
222
+ // cannot carry quick-decision buttons at all; the browser uses
223
+ // window.focus() and the Service Worker bridge.
224
+ hostEnv: "auto",
219
225
  // toast + sound are only useful while the user is elsewhere; the tab
220
226
  // being hidden is the reliable "away" signal.
221
227
  attentionHiddenOnly: false, // needs-input alerts even while the page is visible
@@ -360,6 +366,53 @@
360
366
  return "Notification" in window && Notification.permission === "granted";
361
367
  } catch (e) { return false; } // a policy-disabled Notification must not break delivery
362
368
  }
369
+ // True on the packaged desktop shell, where the app page lives under
370
+ // dsh-app://app inside Electron. The desktop needs its own raise path
371
+ // (raiseWindow below) and cannot carry quick-decision buttons at all.
372
+ // Detection is a page-protocol read; the hostEnv setting can override it
373
+ // for hosts where detection comes out wrong.
374
+ function detectedDesktop() {
375
+ try { return location.protocol === "dsh-app:"; } catch (e) { return false; }
376
+ }
377
+ function isDesktopShell() {
378
+ if (config.hostEnv === "desktop") return true;
379
+ if (config.hostEnv === "web") return false;
380
+ return detectedDesktop();
381
+ }
382
+ // Bring the OS window forward. A browser tab answers to window.focus();
383
+ // the Electron shell does not — DOM focus() never touches the native
384
+ // window, so a toast click there left the session switched but the window
385
+ // buried. The desktop rides the app's own dsh://open deep link instead:
386
+ // the URL is fired through a hidden iframe (the shell's will-navigate and
387
+ // window-open handlers intercept top-level navigations and window.open()
388
+ // to non-http(s) URLs, but not subframe navigations), the OS hands it to
389
+ // the protocol registration, and the single-instance owner restores,
390
+ // shows and focuses the window — the same path the login completion page
391
+ // uses to bring DSH forward.
392
+ //
393
+ // Verified on the real shell (DSH Desktop 0.2.0-rc.2, issue #9): a click
394
+ // on the notification raises the window and switches the conversation in
395
+ // one step, reproducibly. The fire must sit in a user-gesture context —
396
+ // the page-side Notification onclick carries one, and Chromium drops
397
+ // external-protocol navigations launched without it. That is also why
398
+ // gesture-less injections (CDP evaluate, scripted iframe appends) silently
399
+ // produce no protocol launch at all, which read like the channel being
400
+ // blocked when the gesture was the only difference.
401
+ function raiseWindow() {
402
+ try { window.focus(); } catch (e) {}
403
+ if (!isDesktopShell()) return;
404
+ try {
405
+ var f = document.createElement("iframe");
406
+ f.setAttribute("aria-hidden", "true");
407
+ f.setAttribute("tabindex", "-1");
408
+ f.style.cssText = "position:fixed;left:-10px;top:-10px;width:0;height:0;border:0;visibility:hidden";
409
+ f.src = "dsh://open";
410
+ (document.body || document.documentElement).appendChild(f);
411
+ setTimeout(function () {
412
+ try { if (f.parentNode) f.parentNode.removeChild(f); } catch (e) {}
413
+ }, 4000);
414
+ } catch (e) {}
415
+ }
363
416
  // Bring the window forward and make the alerted session the current one,
364
417
  // so a click on the toast lands on the conversation that needs the user
365
418
  // instead of just the app. Navigation moved with the host API: `sessions`
@@ -369,7 +422,7 @@
369
422
  // cordis service that cannot be resolved must still leave a usable window
370
423
  // in front.
371
424
  function focusSession(sessionId) {
372
- try { window.focus(); } catch (e) {}
425
+ raiseWindow();
373
426
  if (!sessionId) return;
374
427
  try {
375
428
  var st = listStore && listStore.getSnapshot();
@@ -1435,7 +1488,7 @@
1435
1488
  });
1436
1489
  } catch (e) {}
1437
1490
  // keep in sync with package.json "version" on every release
1438
- api.version = "1.5.1";
1491
+ api.version = "1.5.3";
1439
1492
  api.setConfig = function (patch) { return setConfigLocal(patch); };
1440
1493
  api.resetConfig = function () {
1441
1494
  try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
@@ -1475,6 +1528,11 @@
1475
1528
  // quick-decision bridge: 'active' is the only state in which approval
1476
1529
  // toasts carry working 同意/拒绝 buttons.
1477
1530
  bridge: swState,
1531
+ // how a toast click gets the OS window forward on this host
1532
+ hostEnv: config.hostEnv,
1533
+ hostEnvDetected: detectedDesktop() ? "desktop" : "web",
1534
+ desktop: isDesktopShell(),
1535
+ raiseChannel: isDesktopShell() ? "dsh-deeplink" : "window-focus",
1478
1536
  actionKeys: Object.keys(pendingActions),
1479
1537
  permission: ("Notification" in window) ? Notification.permission : "n/a",
1480
1538
  visibility: hidden() ? "hidden" : "visible",
@@ -1502,6 +1560,14 @@
1502
1560
  blurb: "当模型停下来需要你操作(审批 / 方案待确认 / 提问),或回复在后台完成时,用系统通知、提示音和标签页标题提醒你。",
1503
1561
  enable: "启用提醒",
1504
1562
  enableHint: "总开关:关闭后不再弹系统通知、不播放提示音,也不再改动标签页标题。",
1563
+ hostEnv: "运行环境",
1564
+ hostEnvHint: "默认自动识别:页面协议是 dsh-app:// 即桌面端(DSH Desktop),其余为 Web 端。识别不准时手动指定:桌面端点通知借 dsh:// 深链把窗口带到前台、快捷裁决按钮不可用;Web 端走 window.focus() 与 Service Worker 桥。",
1565
+ hostEnvAuto: "自动识别(推荐)",
1566
+ hostEnvDesktop: "桌面端(DSH Desktop)",
1567
+ hostEnvWeb: "Web 端(浏览器)",
1568
+ hostEnvMismatch: "(自动识别结果是「",
1569
+ hostEnvMismatchTail: "」,手动选择会覆盖它)",
1570
+ hostEnvDesktopForcedNote: "(当前页面不是 dsh-app://:点通知会尝试唤起 dsh:// 协议,浏览器可能弹「打开应用」确认框)",
1505
1571
  toast: "系统通知",
1506
1572
  toastHint: "经浏览器弹出系统 Toast(Windows 通知中心);需要已授予通知权限。",
1507
1573
  sound: "提示音",
@@ -1521,6 +1587,9 @@
1521
1587
  quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
1522
1588
  bridgeOff: "快捷裁决按钮暂不可用(",
1523
1589
  bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
1590
+ bridgeOffDesktop: "桌面端暂不支持快捷裁决按钮:dsh-app:// 页面无法注册 Service Worker,审批通知只能是不带按钮的普通样式,测试按钮同样只发普通通知。这是宿主限制,不是设置坏了。",
1591
+ testApprovalNoBridge: "已发普通测试通知(快捷裁决按钮需要 Service Worker,当前环境没有,按钮渲染不了)。",
1592
+ testNeedsQuick: "先打开上面的「审批通知上直接裁决」才能测按钮。",
1524
1593
  volume: "音量",
1525
1594
  language: "通知语言",
1526
1595
  langAuto: "跟随界面",
@@ -1537,13 +1606,22 @@
1537
1606
  resetDone: "已恢复默认设置。",
1538
1607
  sent: "已发送测试提醒 · 通知语言:",
1539
1608
  permHint: "通知权限尚未授予:先在页面上点击一次并允许通知,系统通知才能弹出(提示音不受影响)。",
1540
- offHint: "已关闭提醒:先打开「启用提醒」再测试。"
1609
+ offHint: "已关闭提醒:先打开「启用提醒」再测试。",
1610
+ versionLine: "插件版本:dsh-notify-me v"
1541
1611
  };
1542
1612
  var UI_en = {
1543
1613
  nav: "Notify me",
1544
1614
  blurb: "When the agent stops and needs your input (approval / plan review / question) or a reply finishes in the background, get a system notification, a sound, and a tab-title marker.",
1545
1615
  enable: "Enable reminders",
1546
1616
  enableHint: "Master switch: when off, no toasts, no sounds, and the tab title is never modified.",
1617
+ hostEnv: "Environment",
1618
+ hostEnvHint: "Auto-detect by default: a dsh-app:// page is the Desktop (DSH Desktop) shell, anything else is the Web. Override it if detection comes out wrong: on the Desktop a toast click raises the window through the dsh:// deep link and quick-decision buttons are unavailable; on the Web it is window.focus() plus the Service Worker bridge.",
1619
+ hostEnvAuto: "Auto-detect (recommended)",
1620
+ hostEnvDesktop: "Desktop (DSH Desktop)",
1621
+ hostEnvWeb: "Web (browser)",
1622
+ hostEnvMismatch: " (auto-detection sees \"",
1623
+ hostEnvMismatchTail: "\"; the manual choice overrides it)",
1624
+ hostEnvDesktopForcedNote: " (this page is not dsh-app://: a toast click will try to launch the dsh:// protocol and the browser may show an \"open app?\" prompt)",
1547
1625
  toast: "System notifications",
1548
1626
  toastHint: "Native toasts via the browser (Windows notification center); requires notification permission.",
1549
1627
  sound: "Sound",
@@ -1563,6 +1641,9 @@
1563
1641
  quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
1564
1642
  bridgeOff: "Quick-decision buttons are unavailable right now (",
1565
1643
  bridgeOffTail: "); approval toasts fall back to their plain form.",
1644
+ bridgeOffDesktop: "Quick-decision buttons are unavailable on Desktop: dsh-app:// pages cannot register a Service Worker, so approval toasts fall back to their plain, button-less form and the test button only sends a plain notification. This is a host limitation, not a broken setting.",
1645
+ testApprovalNoBridge: "Plain test notification sent (quick-decision buttons need a Service Worker, which this environment lacks, so they cannot render).",
1646
+ testNeedsQuick: "Turn on \"Decide approvals from the toast\" above to test the buttons.",
1566
1647
  volume: "Volume",
1567
1648
  language: "Notification language",
1568
1649
  langAuto: "Follow the interface",
@@ -1579,7 +1660,8 @@
1579
1660
  resetDone: "Defaults restored.",
1580
1661
  sent: "Test alert sent · notification language: ",
1581
1662
  permHint: "Notification permission is not granted yet — click once on the page and allow notifications so toasts can appear (sound is unaffected).",
1582
- offHint: "Reminders are off: enable them first, then test."
1663
+ offHint: "Reminders are off: enable them first, then test.",
1664
+ versionLine: "Plugin version: dsh-notify-me v"
1583
1665
  };
1584
1666
  // Small element helper to keep the hand-written page readable.
1585
1667
  function h(tag, props) {
@@ -1663,19 +1745,45 @@
1663
1745
  testAlert(kind);
1664
1746
  var langName = effectiveLang() === "en" ? t("langEn") : t("langZh");
1665
1747
  var msg = t("sent") + langName;
1666
- if (kind === "approval" && !bridgeActive()) msg += " · " + t("bridgeOff") + swState + t("bridgeOffTail");
1748
+ if (kind === "approval" && !bridgeActive()) {
1749
+ msg += " · " + (isDesktopShell() ? t("testApprovalNoBridge") : t("bridgeOff") + swState + t("bridgeOffTail"));
1750
+ }
1667
1751
  setFeedback(msg);
1668
1752
  }
1669
1753
  var permOK = ("Notification" in window) ? Notification.permission === "granted" : false;
1670
1754
  var resolvedLang = effectiveLang();
1671
1755
  var resolvedLangName = resolvedLang === "en" ? t("langEn") : t("langZh");
1756
+ // The environment row states what detection saw whenever the manual
1757
+ // choice disagrees with it — the override exists for hosts where the
1758
+ // protocol read comes out wrong, so the divergence must be visible.
1759
+ var envHint = t("hostEnvHint");
1760
+ if (cfg.hostEnv === "desktop" && !detectedDesktop()) envHint += t("hostEnvDesktopForcedNote");
1761
+ else if (cfg.hostEnv !== "auto" && isDesktopShell() !== detectedDesktop()) {
1762
+ envHint += t("hostEnvMismatch") + (detectedDesktop() ? t("hostEnvDesktop") : t("hostEnvWeb")) + t("hostEnvMismatchTail");
1763
+ }
1672
1764
  return h("div", { className: "dnm-wrap" }, [
1673
1765
  h("p", { className: "dnm-blurb" }, t("blurb")),
1674
1766
  card([
1675
1767
  rowSwitch("enabled", t("enable"), t("enableHint"), cfg.enabled, function (v) {
1676
1768
  patch({ enabled: v });
1677
1769
  if (!v && attentionActive > 0) { attentionActive = 0; applyTitle(); }
1678
- })
1770
+ }),
1771
+ h("div", { className: "dnm-row" }, [
1772
+ h("div", { className: "dnm-rowText" }, [
1773
+ h("div", { className: "dnm-rowLabel" }, t("hostEnv")),
1774
+ h("div", { className: "dnm-rowHint" }, envHint)
1775
+ ]),
1776
+ h("select", {
1777
+ className: "dnm-select",
1778
+ value: cfg.hostEnv,
1779
+ "aria-label": t("hostEnv"),
1780
+ onChange: function (e) { patch({ hostEnv: e.currentTarget.value }); }
1781
+ }, [
1782
+ h("option", { value: "auto" }, t("hostEnvAuto")),
1783
+ h("option", { value: "desktop" }, t("hostEnvDesktop")),
1784
+ h("option", { value: "web" }, t("hostEnvWeb"))
1785
+ ])
1786
+ ])
1679
1787
  ]),
1680
1788
  card([
1681
1789
  rowSwitch("toast", t("toast"), t("toastHint"), cfg.toast, function (v) { patch({ toast: v }); }),
@@ -1733,9 +1841,12 @@
1733
1841
  ])
1734
1842
  ]),
1735
1843
  cfg.quickActions && cfg.toast && !bridgeActive() ? h("div", { className: "dnm-row" }, [
1736
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("bridgeOff") + swState + t("bridgeOffTail"))])
1844
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail"))])
1737
1845
  ]) : h("div", { className: "dnm-row" }, [
1738
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("quickApprovalNote"))])
1846
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.quickActions ? t("quickApprovalNote") : t("testNeedsQuick"))])
1847
+ ]),
1848
+ h("div", { className: "dnm-row" }, [
1849
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-feedback" }, feedback)])
1739
1850
  ]),
1740
1851
  h("div", { className: "dnm-row" }, [
1741
1852
  h("div", { className: "dnm-rowText" }, [
@@ -1754,7 +1865,9 @@
1754
1865
  h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("permHint"))])
1755
1866
  ]) : null
1756
1867
  ]),
1757
- h("p", { className: "dnm-feedback" }, feedback)
1868
+ // Always-visible version line at the page foot: bug reports need it,
1869
+ // and "which build am I on" should not require the console.
1870
+ h("p", { className: "dnm-hint" }, t("versionLine") + api.version)
1758
1871
  ]);
1759
1872
  }
1760
1873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-me",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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",