dsh-plugin-notify-sounds 1.9.3 → 1.9.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/README.md +8 -5
- package/README.zh.md +9 -6
- package/lib/client.js +128 -26
- package/lib/index.js +130 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Walk away from the window without missing anything: an OS notification whether t
|
|
|
16
16
|
**Install**: DSH → **Plugins** in the left sidebar → Add plugin, paste the line below (keep the version — a release younger than 24 hours is resolved back to the previous one by pnpm's `minimumReleaseAge`) and press **Install**:
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
dsh-plugin-notify-sounds@1.9.
|
|
19
|
+
dsh-plugin-notify-sounds@1.9.5
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -66,7 +66,7 @@ Everything applies immediately — no restart needed.
|
|
|
66
66
|
2. Paste the package name **with a version** (this plugin is published on [npm](https://www.npmjs.com/package/dsh-plugin-notify-sounds)):
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
dsh-plugin-notify-sounds@1.9.
|
|
69
|
+
dsh-plugin-notify-sounds@1.9.5
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Do not drop the version: pnpm's `minimumReleaseAge` policy keeps a release younger than 24 hours out of automatic resolution, so the bare name resolves to the previous one. The current version is on [npm](https://www.npmjs.com/package/dsh-plugin-notify-sounds?activeTab=versions) and in [Releases](https://github.com/BillCx330/dsh-plugin-notify-sounds/releases).
|
|
@@ -79,10 +79,10 @@ The "install source" dropdown above the box picks the download mirror (mainland
|
|
|
79
79
|
|
|
80
80
|
**Upgrades are installed by the package manager**, so the profile's lockfile moves with the files — a DSH plugin cannot install its own update durably (the manager refuses a package that is already declared, and its remove-then-install step unloads the very code that would finish the install). The plugin therefore only *checks*, and tells you where to install:
|
|
81
81
|
|
|
82
|
-
- **
|
|
82
|
+
- **0.2.2 and later**: **Plugins** in the sidebar → the row shows its version state, click update (no uninstall-and-add).
|
|
83
|
+
- **0.2.1 and earlier**: **Plugins** in the left sidebar → uninstall this plugin under *Installed plugins* → **Add plugin** → paste `dsh-plugin-notify-sounds@<version>` → Install → restart DSH. (Keep the version: pnpm's `minimumReleaseAge` policy keeps a freshly published version out of automatic resolution for its first 24 hours, so the bare name can resolve to the previous release.)
|
|
83
84
|
|
|
84
85
|
Uninstalling keeps your settings: preferences and uploaded audio live in the profile (`cordis.patch.yml` and `notify-sounds/sounds/`), so a reinstall restores them — including a clip migrated out of a pre-1.9.0 install.
|
|
85
|
-
- **With the marketplace plugin** ([dshmarket](https://www.npmjs.com/package/dshmarket)): update it in one click there.
|
|
86
86
|
- **On a `web` profile**: `dsh plugin --profile <your profile> add dsh-plugin-notify-sounds@<version>`.
|
|
87
87
|
|
|
88
88
|
The update row also warns when the running version is not what the profile's lockfile records — the state left behind by the pre-1.8.0 self-updater. One reinstall through the package manager fixes it for good.
|
|
@@ -100,7 +100,7 @@ dsh plugin --profile <your profile> add github:BillCx330/dsh-plugin-notify-sound
|
|
|
100
100
|
**Release archive** — download the `.tgz` from [Releases](https://github.com/BillCx330/dsh-plugin-notify-sounds/releases) and install it by its **path on disk**:
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
dsh plugin --profile <your profile> add ./dsh-plugin-notify-sounds-1.9.
|
|
103
|
+
dsh plugin --profile <your profile> add ./dsh-plugin-notify-sounds-1.9.5.tgz
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
(`dsh plugin add` hands its argument straight to the package manager, so it takes a local file path — a download URL will not work.)
|
|
@@ -123,6 +123,7 @@ The command line writes `dsh.profile.bundles` and installs the dependency for yo
|
|
|
123
123
|
- **Versions up to 1.7.0** reach 1.8.0 through their own updater (1.8.0 keeps the same seven filenames), but that arrival is not registered with pnpm: reinstall once through the package manager as above and every later upgrade stays in place. Versions up to 1.6.9 cannot install a release that adds a file — their fixed whitelist refuses it safely; the same one-time reinstall fixes that too.
|
|
124
124
|
- **Multiple windows** elect one page to speak (since 1.7.0): sounds, system notifications and repeat reminders therefore fire once for the whole GUI, not once per window. Title marks stay per-window, because each window has its own title. If the speaking page closes or crashes, another takes over within seconds.
|
|
125
125
|
- **Click-to-return** is fully implemented and tested on Windows; macOS / Linux fall back to the system deep link and are unverified.
|
|
126
|
+
- **A click is slower when the window was closed to the tray**: that case goes through the deep link so Electron can `show()` the window itself (otherwise the frame comes back while the page stays paused) — about 0.5–1.5s slower. On the welcome screen the deep link may do nothing.
|
|
126
127
|
- **Third-party desktop wrappers** (e.g. DSH NEXT) register no `dsh://` protocol and expose no focus API, so clicking a notification cannot return you to the window there — the gap is on the host side, not something a plugin can close.
|
|
127
128
|
- **The window is found by its title** — deliberately without inspecting other processes (that trips security software). If DSH ever changes its title format, click-to-return silently stops working; sounds and notifications are unaffected.
|
|
128
129
|
- **The nav glyph** depends on the settings panel's DOM structure and silently falls back to the official gear after a DSH redesign — cosmetic only.
|
|
@@ -146,6 +147,8 @@ For anyone hacking on the code (exact details live in the source comments):
|
|
|
146
147
|
|
|
147
148
|
## 📜 Changelog
|
|
148
149
|
|
|
150
|
+
- **v1.9.5** (2026-11-07) **Adapted to DSH 0.2.2-rc.1: the Settings glyph is back, at the size of its neighbours.** 0.2.2 wraps the nav glyph in a `<span>`, so the old "find the svg and replace it" patch no longer matched and the cell fell back to the default gear; that release also added the supported `settings.section.icon` slot, so the glyph is now a registered component. Its size was fixed along the way: the alarm clock was drawn in 17 units and rendered into a 16px slot, shrinking it 6% and thinning the stroke from 1.3px to 1.22px; the paths are now refitted to the 16 unit box, so the weight matches the rest of the nav. 0.2.1 keeps the DOM swap; both paths stay. The upgrade notes point at 0.2.2's in-place update, and the in-app update hint follows the DSH version: 0.2.2 and later just say "click Update", older builds still get the full uninstall-and-add steps. The README's marketplace-update bullet is gone.
|
|
151
|
+
- **v1.9.4** (2026-11-03) **Fixed the window appearing frozen after a notification click, and the window not coming back at all with DevTools open.** Two Windows-only defects ([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5), thanks @myzc1000): (1) the raise used Win32 `ShowWindow`, which brings the frame back but leaves the page hidden — the renderer stays paused, so you see the last frame; worse, the window now counts as "visible", so every later click only focuses it and it never recovers. Only a minimised window is restored natively now; a hidden one — or one whose frame is visible while its page is not — goes through `dsh://open`, where Electron's own `show()` restores both. (2) Window matching let any visible window of the shell process outrank a hidden window matched by title, so with DevTools open the inspector was raised instead; titles now always win, DevTools never matches, every candidate must belong to this process, and a blank-titled window remains the last resort. (3) The deep link now goes through `explorer.exe`: the previous launcher **failed silently** while the window was hidden — exit code 0, nothing happened, so a click did nothing at all. **Cost**: clicking while the page is hidden goes through the deep link, so the window takes about 0.5–1.5s longer.
|
|
149
152
|
- **v1.9.3** (2026-11-02) **A visible-but-unfocused window now raises a system notification.** Notifications used to fire only while the page was hidden, but a window sitting in plain sight behind another app — or on a second screen while you type elsewhere — still reports `visible`, so that whole case was silent ([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5), thanks @myzc1000). The gate is now "hidden **or** unfocused". Minimised, tray-hidden and fully covered windows behave as before (Chromium's occlusion tracking marks those hidden).Two small fixes came along: a refused write while adding a clip left its uploaded file behind (it is deleted now), and a multi-line conversation title broke the toast title across lines (it is flattened to one).
|
|
150
153
|
- **v1.9.2** (2026-10-30) **System notifications are titled with the conversation name.** They used to read "Task finished / The agent has finished the current task", so with several conversations open you had to click through to learn which one. The title now carries the conversation's name (from the `title` already on DSH's session rows, falling back to the workspace folder — and treating DSH's own "Working directory: …" fallback as unnamed, so only the folder segment shows), and the body is unchanged; a decision toast prefixes its body with the event so nothing is lost. Titles are trimmed at 48 characters, an untitled session keeps the old wording, and a raw session id is never shown. These fields exist since DSH 0.2.0 — not a new API — and when they are missing the behaviour is exactly what it was. **One cross-platform fix came along**: on Windows two windows uploading the same clip at once made the second report "not audio" (415) — POSIX lets `rename` overwrite, Windows refuses — and that refusal is now treated as success. ([#4](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/4), thanks @AdySnowflake)
|
|
151
154
|
- **v1.9.1** (2026-10-29) **Fixed "too quiet": the nine presets are now levelled by loudness, not by peak.** A user reported that even 100% was quiet. Two things were stacked: the presets had never been matched to each other (`coin` peaks at 0.105, `ding` at 0.315 — 9.5 dB apart), and all of them sat about 12 dB below a decoded clip (a preset played at -16 dBFS at 100%, an imported file at -4). The same slider number meant two loudnesses. The first cut levelled them by peak, which made the coin sound louder still: a square carries 3 dB more energy than a sine at the same peak (RMS/peak 1.0 vs 0.707). Levels now use a crest-corrected peak — sines and squares land on the same energy, and triangles are capped at the ceiling rather than pushed past it. The same number is about 12 dB louder, and the nine sounds agree with each other. **The saved volume is re-scaled once on upgrade**: at the new scale the same number is ~10 dB louder, so the stored value is halved (60 → 30, 100 → 50) — about 3.5 dB above where you were. Turn it up for the new headroom. ([#3](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/3), thanks @sink-er)
|
package/README.zh.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
**安装**:DSH → 左侧栏「插件」→ 添加插件,粘贴下面这行(**带上版本号**,否则发布不满 24 小时的版本会被 pnpm 的 `minimumReleaseAge` 解析回上一版),点「安装」:
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
dsh-plugin-notify-sounds@1.9.
|
|
19
|
+
dsh-plugin-notify-sounds@1.9.5
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -66,7 +66,7 @@ DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别
|
|
|
66
66
|
2. 在输入框粘贴包名**和版本号**(本插件已发布在 [npm](https://www.npmjs.com/package/dsh-plugin-notify-sounds)):
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
dsh-plugin-notify-sounds@1.9.
|
|
69
|
+
dsh-plugin-notify-sounds@1.9.5
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
版本号不要省:pnpm 的 `minimumReleaseAge` 策略会把刚发布不到 24 小时的版本挡在外面,只写包名会解析回上一个版本。想看最新版本号可到 [npm](https://www.npmjs.com/package/dsh-plugin-notify-sounds?activeTab=versions) 或 [Releases](https://github.com/BillCx330/dsh-plugin-notify-sounds/releases) 查。
|
|
@@ -79,10 +79,10 @@ DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别
|
|
|
79
79
|
|
|
80
80
|
**升级由包管理器安装**,这样 profile 的 lockfile 会跟着一起更新——DSH 插件无法自己持久地更新自己(管理器拒绝已声明的包,而它的「先移除再安装」会先卸载掉正要完成安装的代码)。所以插件只负责**检查**,并告诉你从哪里装:
|
|
81
81
|
|
|
82
|
-
-
|
|
82
|
+
- **0.2.2 及以上**:**左侧栏「插件」** → 本插件那一行会显示版本状态,直接点更新(不用再卸载重装)。
|
|
83
|
+
- **0.2.1 及更早**:**左侧栏「插件」** → 在「已安装插件」里卸载本插件 → **添加插件** → 粘贴 `dsh-plugin-notify-sounds@<版本>` → 安装 → 重启 DSH。(务必带版本号:pnpm 的 `minimumReleaseAge` 会让刚发布不到 24 小时的版本不参与自动解析,只写包名会装回上一个版本。)
|
|
83
84
|
|
|
84
85
|
卸载不会丢东西:偏好和自定义音频都存在 profile 里(`cordis.patch.yml` 与 `notify-sounds/sounds/`),重装后自动恢复——包括升级到 1.9.0 时由旧版内嵌音频自动迁移过来的那份。
|
|
85
|
-
- **装了市场插件**([dshmarket](https://www.npmjs.com/package/dshmarket)):在市场里一键更新。
|
|
86
86
|
- **`web` profile**:`dsh plugin --profile <你的 profile> add dsh-plugin-notify-sounds@<版本>`。
|
|
87
87
|
|
|
88
88
|
更新行还会提示「当前版本与 lockfile 记录不一致」——那是 1.8.0 之前的自更新留下的状态,按上面方式重装一次即可永久修好。
|
|
@@ -100,7 +100,7 @@ dsh plugin --profile <你的 profile> add github:BillCx330/dsh-plugin-notify-sou
|
|
|
100
100
|
**Release 压缩包** —— 从 [Releases](https://github.com/BillCx330/dsh-plugin-notify-sounds/releases) 下载 `.tgz`,用它**在磁盘上的路径**安装:
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds-1.9.
|
|
103
|
+
dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds-1.9.5.tgz
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
(`dsh plugin add` 把参数原样交给包管理器,所以要给本地文件路径,不能填下载地址。)
|
|
@@ -123,9 +123,10 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
|
123
123
|
- **1.7.0 及更早版本**可以通过它们自带的自更新到达 1.8.0(1.8.0 保持同样的七个文件名),但这次到达不会被 pnpm 登记:按上面的方式用包管理器重装一次,此后所有升级都会留下。1.6.9 及更早无法安装「新增了文件」的版本——固定白名单会安全拒绝;同样的一次性重装即可解决。
|
|
124
124
|
- **多窗口**:提示音、系统通知和重复提醒由同一个 GUI 的所有页面选出一个「发声页」来发(1.7.0 起),所以同一个事件只响一次;窗口标题标记仍是每个窗口各自一份,因为它们各自有标题。发声页关闭或崩溃后几秒内由其他页面接管。
|
|
125
125
|
- **点击通知回到窗口**:Windows 完整实现并实测;macOS / Linux 走系统深链,未验证。
|
|
126
|
+
- **关闭到后台后点通知会慢一点**:这种情况走系统深链、由 Electron 自己把窗口 `show` 回来(否则画面会停在最后一帧),窗口出现比平时慢约 0.5–1.5 秒;停在欢迎页时深链可能不生效。
|
|
126
127
|
- **第三方封装的桌面客户端**(如 DSH NEXT):唤窗依赖官方壳的 Win32 能力与 `dsh://` 协议注册,封装版没有这两样,点击通知不会回到窗口——那是宿主侧缺的,插件无法补。
|
|
127
128
|
- **唤窗按窗口标题定位主窗口**:刻意不读取其他进程信息(那会触发杀软主动防御)。若将来 DSH 改变标题格式,唤窗会静默失效——提示音与通知本身不受影响。
|
|
128
|
-
-
|
|
129
|
+
- **导航小图标**:0.2.2 起走官方 `settings.section.icon` 插槽;0.2.1 及更早靠替换设置面板里的图标节点,那一代再改版可能回落为官方齿轮(仅影响外观)。
|
|
129
130
|
- **通知权限**:桌面端放行;受限环境(某些浏览器策略)下被拒时,行内会引导手动允许,其余功能不受影响。
|
|
130
131
|
- **macOS 的系统通知**:桌面外壳对权限请求一律放行,所以设置里恒显示「已授权」;**实际是否弹横幅由 macOS 决定**(系统设置 → 通知 → DeepSeek Harness),未签名的开发版可能静默失败——「显示已授权却没通知」多半不是插件的问题。
|
|
131
132
|
|
|
@@ -145,6 +146,8 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
|
145
146
|
|
|
146
147
|
## 📜 更新记录
|
|
147
148
|
|
|
149
|
+
- **v1.9.5**(2026-11-07)**适配 DSH 0.2.2-rc.1:设置页的图标回来了,并且和相邻图标一样大。** 0.2.2 把导航格子改成 `<span>` 包图标,原来那套「找到 svg 再替换」的 DOM 写法匹配不上,图标退回默认齿轮;这一版正好开放了 `settings.section.icon` 插槽,改为按官方方式注册组件。顺手修了尺寸:原来的闹钟按 17 单位画、塞进 16px 槽位,缩了 6%,描边也从 1.3px 变成 1.22px,看着比邻居小一圈;现在按 16 单位重排坐标,描边回到 1.3px。0.2.1 仍走旧的 DOM 替换,两条路都保留。升级说明改成 0.2.2 的「插件页直接更新」;应用内的更新提示也按版本给最短路径:0.2.2 及以上只提示「点更新」,更早的版本才给卸载重装的完整步骤。README 里那条「插件市场一键更新」一并删掉了。
|
|
150
|
+
- **v1.9.4**(2026-11-03)**修好「窗口弹出来但冻住」,以及开 DevTools 后点通知窗口不回来。** 两个 Windows 专属问题([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5),感谢 @myzc1000):① 唤窗以前用 Win32 `ShowWindow` 把窗口显示出来,但 `hide()` 过的窗口页面仍处于隐藏、渲染是停的——窗口框有了,画面停在最后一帧;更糟的是此后系统层面已算「可见」,再点只会走聚焦,永远恢复不了。现在只对**最小化**用原生 restore,隐藏或「框可见但页面隐藏」一律交给 `dsh://open`,由 Electron 自己 `show()`。② 窗口匹配以前让「同进程的任意可见窗口」压过「按标题命中的隐藏窗口」,开着 DevTools 时会选中 DevTools;现在标题永远优先,DevTools 不参与匹配,所有候选必须是本进程的窗口,空标题窗口保留为最后兜底。③ 深链改用 `explorer.exe` 触发:原来那条 shell 命令在窗口隐藏时会**静默失败**(退出码 0,窗口不动),点了等于没反应。**代价**:页面处于隐藏态时点击会走深链,窗口出现慢约 0.5–1.5 秒。
|
|
148
151
|
- **v1.9.3**(2026-11-02)**窗口可见但没焦点时也会弹系统通知。** 此前只在页面不可见时发通知;而窗口被别的程序盖住、或你切到别的窗口打字、DSH 仍完整显示在屏幕上时,页面报告的仍是 `visible`,这类「看得见但没在用」的情况完全收不到通知([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5),感谢 @myzc1000)。现在条件改为「不可见 **或** 没有焦点」。最小化、托盘、被完全遮挡(Chromium 的遮挡检测会把窗口标记为 hidden)行为不变。顺带修了两处小问题:上传音频时若写入被拒,已经存好的文件会残留(现在会一并清掉);对话标题里含换行时会把通知标题撑成两行(现在折叠成一行)。
|
|
149
152
|
- **v1.9.2**(2026-10-30)**系统通知的标题改成对话名。** 此前通知固定写「任务完成 / 智能体已完成当前任务」,同时开着几个对话时,得点进去才知道是哪一个。现在标题用该对话的名字(读的是 DSH 会话列表里已有的 `title`;没有标题、或标题只是「Working directory: 路径」这类兜底串时,只显示目录名),正文不变;决策通知的正文会带上「需要你的决策:」,把原来写在标题里的事件补回来。标题超过 48 字会截断,未命名且无目录时保持原来的文案——**不会显示裸会话 id**。字段从 DSH 0.2.0 起就有,不是新 API,取不到时行为与之前完全一致。**顺带修了一个跨平台问题**:Windows 下两个窗口同时上传同一段音频时,后到的那个会被误报「不是音频」(415)——POSIX 上 `rename` 能覆盖、Windows 上会抛错;现在这种情况按成功处理。([#4](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/4),感谢 @AdySnowflake)
|
|
150
153
|
- **v1.9.1**(2026-10-29)**修好「声音太小」:九个内置音效按实际响度对齐,而不是按峰值。** 有用户反馈 100% 也偏小。查下来是两件事:九个预设从未互相对齐(`coin` 的叠加峰值 0.105、`ding` 0.315,差 9.5 dB);预设整体又比自定义音频轻约 12 dB(预设 100% 时约 -16 dBFS,导入的音频约 -4 dBFS)——同一个音量数字,两种响度。第一版修复按**峰值**对齐,结果金币反而更响:方波在同等峰值下能量比正弦高 3 dB(RMS/峰值 1.0 对 0.707)。现在按**波峰因数折算后的峰值**对齐——正弦和方波落在同一能量上,三角波因为峰值已经触顶不再加推。同样的音量数字比以前响约 12 dB,九个音效之间也一致了。**升级时音量会自动折算**:新刻度下同一个数字比以前响约 10 dB,所以首次启动会把已存的音量减半(60 → 30、100 → 50),实际只比原来高约 3.5 dB;想更大声再往上调即可。([#3](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/3),感谢 @sink-er)
|
package/lib/client.js
CHANGED
|
@@ -92,6 +92,41 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
return typeof navigator !== "undefined" && /Electron/i.test(navigator.userAgent ?? "");
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The Desktop's own version, read off the user agent.
|
|
97
|
+
*
|
|
98
|
+
* Electron puts the app in the UA as `@deepseek-ai/dsh-desktop/<version>`;
|
|
99
|
+
* a browser does not, which is exactly the difference that matters —
|
|
100
|
+
* outside the shell there is no version to branch on.
|
|
101
|
+
*
|
|
102
|
+
* @returns `major.minor.patch`, or undefined outside the Desktop.
|
|
103
|
+
*/
|
|
104
|
+
function desktopVersion() {
|
|
105
|
+
const ua = typeof navigator === "undefined" ? "" : String(navigator.userAgent);
|
|
106
|
+
const match = /@deepseek-ai\/dsh-desktop\/(\d+\.\d+\.\d+)/iu.exec(ua);
|
|
107
|
+
return match === null ? undefined : match[1];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether a `major.minor.patch` string is at least `floor`.
|
|
112
|
+
*
|
|
113
|
+
* Numeric per segment rather than string order: `0.10.0` is newer than
|
|
114
|
+
* `0.9.0`, and a lexical compare gets that backwards.
|
|
115
|
+
*
|
|
116
|
+
* @param version - the version to test, or undefined when unknown.
|
|
117
|
+
* @param floor - the lowest version that counts.
|
|
118
|
+
* @returns whether the version is known and meets the floor.
|
|
119
|
+
*/
|
|
120
|
+
function atLeastVersion(version, floor) {
|
|
121
|
+
if (typeof version !== "string") return false;
|
|
122
|
+
const a = version.split(".").map(Number);
|
|
123
|
+
const b = floor.split(".").map(Number);
|
|
124
|
+
for (let i = 0; i < 3; i += 1) {
|
|
125
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
/**
|
|
96
131
|
* Whether the page is on macOS.
|
|
97
132
|
*
|
|
@@ -255,6 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
255
290
|
"update.available": "有新版",
|
|
256
291
|
"update.failed": "检查失败",
|
|
257
292
|
"update.failedDetail": "两个源都没有给出可用的答复:可能是网络、代理或镜像源临时不可用,稍后重试即可",
|
|
293
|
+
"update.desktopGuidanceInPlace": "升级:左侧栏「插件」→ 找到本插件 → 直接点「更新」,不用卸载重装(管理器会自己选版本)",
|
|
258
294
|
"update.desktopGuidance": "升级走包管理器:左侧栏「插件」→ 在「已安装插件」里卸载本插件 → 点「添加插件」→ 粘贴 dsh-plugin-notify-sounds@{version} → 安装 → 重启 DSH",
|
|
259
295
|
"update.versionPinned": "务必带版本号:发布不满 24 小时的版本会被 pnpm 的 minimumReleaseAge 策略挡掉,只写包名会装回上一个版本",
|
|
260
296
|
"update.webGuidance": "在终端执行 dsh plugin --profile {profile} add dsh-plugin-notify-sounds@{version}",
|
|
@@ -350,6 +386,7 @@ window.__ModuleLoader__.load({
|
|
|
350
386
|
"update.available": "New version available",
|
|
351
387
|
"update.failed": "Check failed",
|
|
352
388
|
"update.failedDetail": "Neither source returned a usable answer — the network, a proxy or a mirror may be temporarily unavailable. Try again later.",
|
|
389
|
+
"update.desktopGuidanceInPlace": "To update: Plugins in the sidebar → find this plugin → click Update. No uninstall, and the manager picks the version.",
|
|
353
390
|
"update.desktopGuidance": "Update through the package manager: sidebar Plugins → uninstall this plugin under Installed plugins → Add plugin → paste dsh-plugin-notify-sounds@{version} → Install → restart DSH",
|
|
354
391
|
"update.versionPinned": "Keep the version: pnpm's minimumReleaseAge policy blocks a release for its first 24 hours, so the bare name resolves to the previous version",
|
|
355
392
|
"update.webGuidance": "Run dsh plugin --profile {profile} add dsh-plugin-notify-sounds@{version} in a terminal",
|
|
@@ -1322,8 +1359,13 @@ window.__ModuleLoader__.load({
|
|
|
1322
1359
|
// here, and in a sandboxed renderer either can raise. When
|
|
1323
1360
|
// they ran ahead of the fetch, a throw left the click a
|
|
1324
1361
|
// silent no-op with no request ever sent.
|
|
1362
|
+
// The Host can see the window but not the page: a window
|
|
1363
|
+
// that is merely behind another app and one left "fake
|
|
1364
|
+
// visible" by an earlier raise look identical over Win32.
|
|
1365
|
+
// Only this side knows which it is, so the click carries it.
|
|
1325
1366
|
try {
|
|
1326
|
-
|
|
1367
|
+
const vis = typeof document !== "undefined" && document.visibilityState === "visible" ? "visible" : "hidden";
|
|
1368
|
+
fetch(`${FOCUS_PATH}?vis=${vis}`, { method: "POST" }).catch(() => {});
|
|
1327
1369
|
} catch {}
|
|
1328
1370
|
// `window.focus()` only focuses the page, not the OS
|
|
1329
1371
|
// window. The route above restores and focuses the real
|
|
@@ -2354,7 +2396,15 @@ window.__ModuleLoader__.load({
|
|
|
2354
2396
|
if (installTarget !== undefined && installTarget !== "") {
|
|
2355
2397
|
const version = packageVersion(installTarget);
|
|
2356
2398
|
let guidance;
|
|
2357
|
-
|
|
2399
|
+
// 0.2.2 updates a third-party package from the plugin page
|
|
2400
|
+
// itself, so the uninstall-and-add steps are the long way round
|
|
2401
|
+
// there. Version-branch rather than time-branch: plenty of
|
|
2402
|
+
// installs stay on 0.2.1, where those steps are still the only
|
|
2403
|
+
// route.
|
|
2404
|
+
const inPlace = isDesktopShell() && atLeastVersion(desktopVersion(), "0.2.2");
|
|
2405
|
+
if (inPlace) {
|
|
2406
|
+
guidance = t("update.desktopGuidanceInPlace");
|
|
2407
|
+
} else if (isDesktopShell()) {
|
|
2358
2408
|
guidance = t("update.desktopGuidance", { version });
|
|
2359
2409
|
} else if (typeof payload.profile === "string" && payload.profile !== "") {
|
|
2360
2410
|
guidance = t("update.webGuidance", { profile: payload.profile, version });
|
|
@@ -2368,8 +2418,10 @@ window.__ModuleLoader__.load({
|
|
|
2368
2418
|
lines.push(h("div", { className: css.hint, key: "howto" }, guidance));
|
|
2369
2419
|
// Its own line: the steps above are what to do, this is why the
|
|
2370
2420
|
// version cannot be dropped — and a parcel of text this long
|
|
2371
|
-
// reads as one dense block when it is glued onto the steps.
|
|
2372
|
-
|
|
2421
|
+
// reads as one dense block when it is glued onto the steps. The
|
|
2422
|
+
// in-place route types no version at all, so the note would only
|
|
2423
|
+
// puzzle.
|
|
2424
|
+
if (!inPlace) lines.push(h("div", { className: css.hint, key: "pinned" }, t("update.versionPinned")));
|
|
2373
2425
|
}
|
|
2374
2426
|
return h("div", { className: css.row }, [
|
|
2375
2427
|
h("div", { key: "text" }, [
|
|
@@ -2393,13 +2445,15 @@ window.__ModuleLoader__.load({
|
|
|
2393
2445
|
|
|
2394
2446
|
//#region settings nav icon
|
|
2395
2447
|
/**
|
|
2396
|
-
*
|
|
2397
|
-
* (`navIcon` in ui-settings-general)
|
|
2398
|
-
*
|
|
2399
|
-
*
|
|
2400
|
-
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2448
|
+
* Until 0.2.2 the shell drew every nav glyph from a hardcoded id → icon
|
|
2449
|
+
* map (`navIcon` in ui-settings-general), so an id outside that map —
|
|
2450
|
+
* ours — fell back to the settings gear and the only way to change it
|
|
2451
|
+
* was to swap the cell's SVG from the DOM. That is what the patch below
|
|
2452
|
+
* still does, and only for 0.2.1.
|
|
2453
|
+
*
|
|
2454
|
+
* 0.2.2 added `settings.section.icon`, so the glyph is a component
|
|
2455
|
+
* registered under the section id and the DOM patch is dead weight
|
|
2456
|
+
* there (its tag check no longer matches the shell's new markup).
|
|
2403
2457
|
*
|
|
2404
2458
|
* The swap is cosmetic DOM surgery, deliberately narrow: it matches the
|
|
2405
2459
|
* one nav cell whose label is this section's, copies the original SVG's
|
|
@@ -2415,20 +2469,55 @@ window.__ModuleLoader__.load({
|
|
|
2415
2469
|
/** Marker attribute distinguishing a swapped glyph from the shipped one. */
|
|
2416
2470
|
const NAV_ICON_MARKER = "data-notify-sounds-icon";
|
|
2417
2471
|
|
|
2418
|
-
/**
|
|
2472
|
+
/**
|
|
2473
|
+
* The section's glyph: the shell's own alarm clock, refitted to the 16
|
|
2474
|
+
* unit box its nav icons use.
|
|
2475
|
+
*
|
|
2476
|
+
* The artwork is the primitive family's (`IconAlarmClockOutlineMedium`),
|
|
2477
|
+
* drawn in 17 units. Rendering it straight into a 16px slot shrank it by
|
|
2478
|
+
* 6% and thinned the stroke from 1.3px to 1.22px, which read as a
|
|
2479
|
+
* smaller icon than its neighbours. These paths are the same geometry
|
|
2480
|
+
* scaled by 16/17 × 1.02 and recentred on (8, 8): same artwork, the
|
|
2481
|
+
* weight the rest of the nav has.
|
|
2482
|
+
*/
|
|
2419
2483
|
const NAV_ICON_PATHS = [
|
|
2420
|
-
"
|
|
2421
|
-
"M12.
|
|
2422
|
-
"M8.
|
|
2423
|
-
"M8.
|
|
2424
|
-
"M1.
|
|
2425
|
-
"M14.
|
|
2484
|
+
"M3.968 12.021L2.973 14.111",
|
|
2485
|
+
"M12.174 12.021L13.169 14.111",
|
|
2486
|
+
"M8.071 4.672V7.987L10.621 9.517",
|
|
2487
|
+
"M8.071 13.47C11.169 13.47 13.681 10.958 13.681 7.86C13.681 4.761 11.169 2.25 8.071 2.25C4.973 2.25 2.461 4.761 2.461 7.86C2.461 10.958 4.973 13.47 8.071 13.47Z",
|
|
2488
|
+
"M1.587 4.63C2.145 3.522 2.981 2.577 4.004 1.889",
|
|
2489
|
+
"M14.413 4.63C13.855 3.522 13.019 2.577 11.996 1.889",
|
|
2426
2490
|
];
|
|
2427
2491
|
|
|
2428
2492
|
/**
|
|
2429
|
-
*
|
|
2493
|
+
* The section's nav glyph as a component.
|
|
2494
|
+
*
|
|
2495
|
+
* Since 0.2.2 the shell offers `settings.section.icon`: one component
|
|
2496
|
+
* per section id, rendered inside the nav cell, sized by the shell. That
|
|
2497
|
+
* is the supported route and what this renders through — the DOM patch
|
|
2498
|
+
* further down only exists for 0.2.1, where the shell had no such slot.
|
|
2499
|
+
*
|
|
2500
|
+
* @param props - `size` in pixels and whether the section is `active`.
|
|
2501
|
+
* @returns the glyph.
|
|
2502
|
+
*/
|
|
2503
|
+
function NotifySoundsNavIcon({ size = 16 }) {
|
|
2504
|
+
return h("svg", {
|
|
2505
|
+
width: size,
|
|
2506
|
+
height: size,
|
|
2507
|
+
viewBox: "0 0 16 16",
|
|
2508
|
+
fill: "none",
|
|
2509
|
+
stroke: "currentColor",
|
|
2510
|
+
strokeWidth: 1.3,
|
|
2511
|
+
strokeLinecap: "round",
|
|
2512
|
+
"aria-hidden": true,
|
|
2513
|
+
}, NAV_ICON_PATHS.map((d) => h("path", { key: d, d })));
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/**
|
|
2517
|
+
* Build the replacement glyph with the shell's icon geometry: `fill=none`
|
|
2430
2518
|
* on the root (paths inherit it), `stroke=currentColor` so the shell's
|
|
2431
|
-
* nav color rules keep driving it, and the medium 1.3px stroke
|
|
2519
|
+
* nav color rules keep driving it, and the medium 1.3px stroke with
|
|
2520
|
+
* round caps.
|
|
2432
2521
|
*
|
|
2433
2522
|
* @returns a detached SVG element.
|
|
2434
2523
|
*/
|
|
@@ -2436,18 +2525,18 @@ window.__ModuleLoader__.load({
|
|
|
2436
2525
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2437
2526
|
svg.setAttribute("width", "16");
|
|
2438
2527
|
svg.setAttribute("height", "16");
|
|
2439
|
-
//
|
|
2440
|
-
//
|
|
2441
|
-
//
|
|
2442
|
-
|
|
2443
|
-
svg.setAttribute("viewBox", "0 0 17 17");
|
|
2528
|
+
// 16×16, the same box the shell's own nav glyphs use: a 17-unit
|
|
2529
|
+
// artwork rendered into the 16px slot came out smaller than its
|
|
2530
|
+
// neighbours.
|
|
2531
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
2444
2532
|
svg.setAttribute("fill", "none");
|
|
2533
|
+
svg.setAttribute("stroke", "currentColor");
|
|
2445
2534
|
svg.setAttribute("stroke-width", "1.3");
|
|
2535
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
2446
2536
|
svg.setAttribute("aria-hidden", "true");
|
|
2447
2537
|
for (const d of NAV_ICON_PATHS) {
|
|
2448
2538
|
const path = document.createElementNS(svg.namespaceURI, "path");
|
|
2449
2539
|
path.setAttribute("d", d);
|
|
2450
|
-
path.setAttribute("stroke", "currentColor");
|
|
2451
2540
|
svg.appendChild(path);
|
|
2452
2541
|
}
|
|
2453
2542
|
return svg;
|
|
@@ -3466,8 +3555,21 @@ window.__ModuleLoader__.load({
|
|
|
3466
3555
|
onRemoveSound: (slot) => removeSound(slot),
|
|
3467
3556
|
}),
|
|
3468
3557
|
}, NotifySoundsSection));
|
|
3558
|
+
// 0.2.2's supported route. A shell without the slot (0.2.1) throws
|
|
3559
|
+
// here — or ignores it — and the DOM patch below still swaps the
|
|
3560
|
+
// gear, so both generations show the same glyph.
|
|
3561
|
+
let disposeIcon = () => {};
|
|
3562
|
+
try {
|
|
3563
|
+
disposeIcon = ctx.slots.inject("settings.section.icon", () => ctx.slots.register({
|
|
3564
|
+
name: "settings.section.icon",
|
|
3565
|
+
id: SETTINGS_NAMESPACE,
|
|
3566
|
+
order: 5,
|
|
3567
|
+
locale: NS,
|
|
3568
|
+
}, NotifySoundsNavIcon));
|
|
3569
|
+
} catch {}
|
|
3469
3570
|
const disposeNavIcon = installNavIconPatch(t);
|
|
3470
3571
|
return () => {
|
|
3572
|
+
disposeIcon();
|
|
3471
3573
|
disposeNavIcon();
|
|
3472
3574
|
disposeSection();
|
|
3473
3575
|
};
|
package/lib/index.js
CHANGED
|
@@ -170,6 +170,9 @@ function loadNativeRaise() {
|
|
|
170
170
|
EnumWindowsProc,
|
|
171
171
|
EnumWindows: user32.func("bool __stdcall EnumWindows(void *cb, intptr_t param)"),
|
|
172
172
|
IsWindowVisible: user32.func("bool __stdcall IsWindowVisible(void *h)"),
|
|
173
|
+
// Minimised is a separate state from hidden: the page keeps rendering,
|
|
174
|
+
// so restoring it needs nothing from Electron.
|
|
175
|
+
IsIconic: user32.func("bool __stdcall IsIconic(void *h)"),
|
|
173
176
|
GetWindowTextLengthW: user32.func("int __stdcall GetWindowTextLengthW(void *h)"),
|
|
174
177
|
GetWindowTextW: user32.func("int __stdcall GetWindowTextW(void *h, _Out_ uint16 *buf, int max)"),
|
|
175
178
|
ShowWindow: user32.func("bool __stdcall ShowWindow(void *h, int cmd)"),
|
|
@@ -228,7 +231,18 @@ export function windowTitle(native, handle) {
|
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
/**
|
|
231
|
-
*
|
|
234
|
+
* Titles that belong to a DevTools window rather than to the app.
|
|
235
|
+
*
|
|
236
|
+
* Chromium titles its inspector "<DevTools> - <inspected title>", and the
|
|
237
|
+
* inspected title already ends with the app name — so a plain `endsWith`
|
|
238
|
+
* check would treat the inspector as the main window. Only a PREFIX followed
|
|
239
|
+
* by a separator counts: a conversation that merely talks about DevTools keeps
|
|
240
|
+
* its own title and must still match.
|
|
241
|
+
*/
|
|
242
|
+
const DEVTOOLS_TITLE = /^(?:devtools|开发者工具)\s*[-–—:]/iu;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Whether a window's title names the Desktop.
|
|
232
246
|
*
|
|
233
247
|
* The title is whatever the page last set — the shell only overrides it for
|
|
234
248
|
* its own error screens — so it is `<session or screen title> — <app name>`
|
|
@@ -243,6 +257,7 @@ export function windowTitle(native, handle) {
|
|
|
243
257
|
* Exported for the Host-half tests: the one pure piece of the Win32 search.
|
|
244
258
|
*/
|
|
245
259
|
export function isDesktopTitle(title) {
|
|
260
|
+
if (DEVTOOLS_TITLE.test(title)) return false;
|
|
246
261
|
return title === DESKTOP_APP_TITLE
|
|
247
262
|
|| title.endsWith(`— ${DESKTOP_APP_TITLE}`)
|
|
248
263
|
|| title.endsWith(`- ${DESKTOP_APP_TITLE}`);
|
|
@@ -251,12 +266,15 @@ export function isDesktopTitle(title) {
|
|
|
251
266
|
/**
|
|
252
267
|
* Find the Desktop's primary window.
|
|
253
268
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
269
|
+
* Two signals, deliberately ranked: a title that names the app is strong, "a
|
|
270
|
+
* window owned by the shell process" is weak. The weak one only exists because
|
|
271
|
+
* a repainting window can briefly answer with an empty title; it must never
|
|
272
|
+
* outrank a title match, or the inspector — a visible window owned by the same
|
|
273
|
+
* process — wins while the real window sits hidden in the tray.
|
|
274
|
+
*
|
|
275
|
+
* Every candidate has to belong to the shell process when that is known: a
|
|
276
|
+
* second installation with the same title would otherwise be raised by
|
|
277
|
+
* mistake.
|
|
260
278
|
*
|
|
261
279
|
* @param native - the bound Win32 calls.
|
|
262
280
|
* @returns the window handle, or undefined.
|
|
@@ -267,8 +285,15 @@ export function findDesktopWindow(native) {
|
|
|
267
285
|
let byPidVisible;
|
|
268
286
|
let byPidHidden;
|
|
269
287
|
const pidBuffer = new Uint32Array(1);
|
|
288
|
+
const ownedByShell = (handle) => {
|
|
289
|
+
if (SHELL_PID === undefined || SHELL_PID === 0) return true;
|
|
290
|
+
pidBuffer[0] = 0;
|
|
291
|
+
native.GetWindowThreadProcessId(handle, pidBuffer);
|
|
292
|
+
return pidBuffer[0] === SHELL_PID;
|
|
293
|
+
};
|
|
270
294
|
const visit = native.koffi.register((handle) => {
|
|
271
295
|
try {
|
|
296
|
+
if (!ownedByShell(handle)) return true;
|
|
272
297
|
const visible = native.IsWindowVisible(handle);
|
|
273
298
|
const title = windowTitle(native, handle);
|
|
274
299
|
if (isDesktopTitle(title)) {
|
|
@@ -279,12 +304,15 @@ export function findDesktopWindow(native) {
|
|
|
279
304
|
if (byTitleHidden === undefined) byTitleHidden = handle;
|
|
280
305
|
return true;
|
|
281
306
|
}
|
|
282
|
-
// A
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
307
|
+
// A DevTools window is never the one to raise.
|
|
308
|
+
if (DEVTOOLS_TITLE.test(title)) return true;
|
|
309
|
+
// An empty title is the transient case the last resort exists for:
|
|
310
|
+
// it stays usable, but only as a HIDDEN candidate, so a visible
|
|
311
|
+
// helper window cannot answer for the main one.
|
|
312
|
+
if (title === "") {
|
|
313
|
+
if (!visible && byPidHidden === undefined) byPidHidden = handle;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
288
316
|
if (visible) {
|
|
289
317
|
if (byPidVisible === undefined) byPidVisible = handle;
|
|
290
318
|
} else if (byPidHidden === undefined) {
|
|
@@ -304,12 +332,40 @@ export function findDesktopWindow(native) {
|
|
|
304
332
|
native.koffi.unregister(visit);
|
|
305
333
|
} catch {}
|
|
306
334
|
}
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
335
|
+
// Title before pid, and within each signal a visible window before a
|
|
336
|
+
// hidden one: a stale hidden instance must not outrank the live window.
|
|
337
|
+
return byTitleVisible ?? byTitleHidden ?? byPidVisible ?? byPidHidden;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Returned when only Electron can bring the window back: see `raiseIntent`. */
|
|
341
|
+
export const RAISE_HIDDEN = "hidden";
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* What a raise should do for a window in this state.
|
|
345
|
+
*
|
|
346
|
+
* Three states look alike over Win32 and are not:
|
|
347
|
+
*
|
|
348
|
+
* - `iconic` — minimised. The page kept rendering, so restoring the frame is
|
|
349
|
+
* enough and Electron does not have to be involved.
|
|
350
|
+
* - `!visible` — hidden (closed to the tray). The frame is gone AND the page
|
|
351
|
+
* was told to stop; Win32 cannot undo the second half.
|
|
352
|
+
* - `pageHidden` on a visible window — either the window is covered by
|
|
353
|
+
* another app (Chromium marks it hidden) or a previous raise left it
|
|
354
|
+
* "fake visible": the frame is back but the page never resumed. Only
|
|
355
|
+
* Electron's own show() fixes the second case, and the host cannot tell
|
|
356
|
+
* the two apart, so both take the Electron path.
|
|
357
|
+
*
|
|
358
|
+
* Returning `"hidden"` is what keeps a frozen window from staying frozen: a
|
|
359
|
+
* raw ShowWindow would re-show the frame while the page stays paused, and every
|
|
360
|
+
* later click would look at a visible window and never try again.
|
|
361
|
+
*
|
|
362
|
+
* @param state - the window as Win32 and the page report it.
|
|
363
|
+
* @returns `"restore"`, `"native"`, or `"hidden"`.
|
|
364
|
+
*/
|
|
365
|
+
export function raiseIntent({ iconic, visible, pageHidden }) {
|
|
366
|
+
if (iconic) return "restore";
|
|
367
|
+
if (pageHidden || !visible) return "hidden";
|
|
368
|
+
return "native";
|
|
313
369
|
}
|
|
314
370
|
|
|
315
371
|
/**
|
|
@@ -317,18 +373,20 @@ export function findDesktopWindow(native) {
|
|
|
317
373
|
*
|
|
318
374
|
* @returns whether the window actually ended up in the foreground.
|
|
319
375
|
*/
|
|
320
|
-
function raiseDesktopWindowNative() {
|
|
376
|
+
function raiseDesktopWindowNative(pageHidden = false) {
|
|
321
377
|
const native = loadNativeRaise();
|
|
322
378
|
if (native === undefined) return false;
|
|
323
379
|
const handle = findDesktopWindow(native);
|
|
324
380
|
if (handle === undefined) return false;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
381
|
+
const intent = raiseIntent({
|
|
382
|
+
iconic: native.IsIconic(handle),
|
|
383
|
+
visible: native.IsWindowVisible(handle),
|
|
384
|
+
pageHidden,
|
|
385
|
+
});
|
|
386
|
+
if (intent === "hidden") return RAISE_HIDDEN;
|
|
387
|
+
// A minimised window restores with the frame alone; one that is simply
|
|
388
|
+
// visible needs nothing shown at all.
|
|
389
|
+
if (intent === "restore") native.ShowWindow(handle, SW_RESTORE);
|
|
332
390
|
native.BringWindowToTop(handle);
|
|
333
391
|
// The foreground request is the part Windows polices: a background process
|
|
334
392
|
// may only take the foreground under some conditions, so on its own this is
|
|
@@ -384,17 +442,25 @@ function raiseDesktopWindowNative() {
|
|
|
384
442
|
* the fallback and not the primary path.
|
|
385
443
|
*/
|
|
386
444
|
function raiseDesktopWindowDeepLink() {
|
|
387
|
-
// `
|
|
388
|
-
//
|
|
445
|
+
// `explorer.exe` hands the URL to the registered protocol handler and
|
|
446
|
+
// exits. Its exit code is 1 even when the hand-off worked, so the code is
|
|
447
|
+
// no verdict — only a spawn error is one. That is why this does not go
|
|
448
|
+
// through `launcher()`, whose contract is "exit 0 means it worked".
|
|
389
449
|
//
|
|
390
|
-
// `
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
450
|
+
// `cmd /c start "" <url>` was the original route and is kept only as a
|
|
451
|
+
// fallback: measured on 0.2.1-rc.5 with the window hidden, it exits 0 and
|
|
452
|
+
// the window stays hidden — a silent failure that looked like success.
|
|
453
|
+
//
|
|
454
|
+
// `spawn`, NOT `spawnSync`: this is the last resort, and blocking the
|
|
455
|
+
// Host's event loop would stall every other HTTP request — including
|
|
456
|
+
// streamed output — while the user waits on a click.
|
|
457
|
+
try {
|
|
458
|
+
const child = spawn("explorer.exe", [FOCUS_DEEP_LINK], { windowsHide: true, stdio: "ignore" });
|
|
459
|
+
child.on("error", () => launcher("cmd.exe", ["/d", "/s", "/c", "start", "", FOCUS_DEEP_LINK]));
|
|
460
|
+
child.unref();
|
|
461
|
+
} catch {
|
|
462
|
+
launcher("cmd.exe", ["/d", "/s", "/c", "start", "", FOCUS_DEEP_LINK]);
|
|
463
|
+
}
|
|
398
464
|
}
|
|
399
465
|
|
|
400
466
|
/**
|
|
@@ -485,10 +551,10 @@ const warned = new Set();
|
|
|
485
551
|
*
|
|
486
552
|
* Every failure mode leaves the click a harmless no-op.
|
|
487
553
|
*/
|
|
488
|
-
function tryRaiseNative() {
|
|
554
|
+
function tryRaiseNative(pageHidden = false) {
|
|
489
555
|
try {
|
|
490
556
|
if (process.platform !== "win32") return false;
|
|
491
|
-
return raiseDesktopWindowNative();
|
|
557
|
+
return raiseDesktopWindowNative(pageHidden);
|
|
492
558
|
} catch (error) {
|
|
493
559
|
warn(`native raise failed: ${error?.message ?? error}`);
|
|
494
560
|
return false;
|
|
@@ -1183,19 +1249,34 @@ export function apply(ctx) {
|
|
|
1183
1249
|
// window entirely.
|
|
1184
1250
|
const headers = req.headers ?? {};
|
|
1185
1251
|
const fromBrowser = headers.origin !== undefined || headers["sec-fetch-site"] !== undefined;
|
|
1252
|
+
// The page knows something Win32 cannot see: whether its renderer
|
|
1253
|
+
// was told to stop. That is the difference between a window that
|
|
1254
|
+
// is merely behind another app and one left "fake visible" by an
|
|
1255
|
+
// earlier raise, which frames alone never recover from.
|
|
1256
|
+
const pageHidden = new URL(req.url ?? "/", "http://localhost").searchParams.get("vis") === "hidden";
|
|
1186
1257
|
res.statusCode = 204;
|
|
1187
1258
|
res.end();
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1259
|
+
if (fromBrowser) {
|
|
1260
|
+
// A browser brings its own window forward; raising the Desktop
|
|
1261
|
+
// instead would be the wrong window entirely.
|
|
1262
|
+
log(`focus click (fromBrowser=true, pageHidden=${pageHidden}, outcome=browser)`);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
// A hidden window is not a matter of retrying — the frame is not
|
|
1266
|
+
// the problem, so only Electron can show it. Retrying would just
|
|
1267
|
+
// stack 540ms in front of the same deep link.
|
|
1268
|
+
const attempts = pageHidden ? 1 : 3;
|
|
1269
|
+
let outcome = false;
|
|
1270
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1271
|
+
outcome = tryRaiseNative(pageHidden);
|
|
1272
|
+
if (outcome === true || outcome === RAISE_HIDDEN) break;
|
|
1273
|
+
if (attempt < attempts - 1) await new Promise((resolve) => setTimeout(resolve, 180));
|
|
1274
|
+
}
|
|
1275
|
+
if (outcome !== true) {
|
|
1276
|
+
log(`focus: ${outcome === RAISE_HIDDEN ? "window is hidden, Electron must show it" : "native raise unavailable"} (pageHidden=${pageHidden})`);
|
|
1277
|
+
focusDesktopWindowDeepLink();
|
|
1198
1278
|
}
|
|
1279
|
+
log(`focus click (fromBrowser=false, pageHidden=${pageHidden}, outcome=${String(outcome)})`);
|
|
1199
1280
|
},
|
|
1200
1281
|
}), "notify-sounds: focus route");
|
|
1201
1282
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-notify-sounds",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "DeepSeek Harness 原生风格提示音插件:需要决策或任务完成时播放提示音,点击系统通知可回到窗口;支持重复提醒、静音时段,以及目标模式下只在最终轮提醒。 · Native-style notification sounds for DeepSeek Harness: a chime when the agent needs your decision or finishes a task, click a system notification to jump back, plus repeat reminders, quiet hours, and goal-mode alerts that wait for the final round.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|