dsh-plugin-notify-sounds 1.9.3 → 1.9.4
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 +5 -3
- package/README.zh.md +5 -3
- package/lib/client.js +6 -1
- 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.4
|
|
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.4
|
|
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).
|
|
@@ -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.4.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,7 @@ For anyone hacking on the code (exact details live in the source comments):
|
|
|
146
147
|
|
|
147
148
|
## 📜 Changelog
|
|
148
149
|
|
|
150
|
+
- **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)): (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
151
|
- **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
152
|
- **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
153
|
- **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.4
|
|
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.4
|
|
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) 查。
|
|
@@ -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.4.tgz
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
(`dsh plugin add` 把参数原样交给包管理器,所以要给本地文件路径,不能填下载地址。)
|
|
@@ -123,6 +123,7 @@ 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
|
- **导航小图标**:依赖设置面板的 DOM 结构,若DSH 改版后自动回落为官方齿轮,仅影响外观。
|
|
@@ -145,6 +146,7 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
|
145
146
|
|
|
146
147
|
## 📜 更新记录
|
|
147
148
|
|
|
149
|
+
- **v1.9.4**(2026-11-03)**修好「窗口弹出来但冻住」,以及开 DevTools 后点通知窗口不回来。** 两个 Windows 专属问题([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5)):① 唤窗以前用 Win32 `ShowWindow` 把窗口显示出来,但 `hide()` 过的窗口页面仍处于隐藏、渲染是停的——窗口框有了,画面停在最后一帧;更糟的是此后系统层面已算「可见」,再点只会走聚焦,永远恢复不了。现在只对**最小化**用原生 restore,隐藏或「框可见但页面隐藏」一律交给 `dsh://open`,由 Electron 自己 `show()`。② 窗口匹配以前让「同进程的任意可见窗口」压过「按标题命中的隐藏窗口」,开着 DevTools 时会选中 DevTools;现在标题永远优先,DevTools 不参与匹配,所有候选必须是本进程的窗口,空标题窗口保留为最后兜底。③ 深链改用 `explorer.exe` 触发:原来那条 shell 命令在窗口隐藏时会**静默失败**(退出码 0,窗口不动),点了等于没反应。**代价**:页面处于隐藏态时点击会走深链,窗口出现慢约 0.5–1.5 秒。
|
|
148
150
|
- **v1.9.3**(2026-11-02)**窗口可见但没焦点时也会弹系统通知。** 此前只在页面不可见时发通知;而窗口被别的程序盖住、或你切到别的窗口打字、DSH 仍完整显示在屏幕上时,页面报告的仍是 `visible`,这类「看得见但没在用」的情况完全收不到通知([#5](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/5),感谢 @myzc1000)。现在条件改为「不可见 **或** 没有焦点」。最小化、托盘、被完全遮挡(Chromium 的遮挡检测会把窗口标记为 hidden)行为不变。顺带修了两处小问题:上传音频时若写入被拒,已经存好的文件会残留(现在会一并清掉);对话标题里含换行时会把通知标题撑成两行(现在折叠成一行)。
|
|
149
151
|
- **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
152
|
- **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
|
@@ -1322,8 +1322,13 @@ window.__ModuleLoader__.load({
|
|
|
1322
1322
|
// here, and in a sandboxed renderer either can raise. When
|
|
1323
1323
|
// they ran ahead of the fetch, a throw left the click a
|
|
1324
1324
|
// silent no-op with no request ever sent.
|
|
1325
|
+
// The Host can see the window but not the page: a window
|
|
1326
|
+
// that is merely behind another app and one left "fake
|
|
1327
|
+
// visible" by an earlier raise look identical over Win32.
|
|
1328
|
+
// Only this side knows which it is, so the click carries it.
|
|
1325
1329
|
try {
|
|
1326
|
-
|
|
1330
|
+
const vis = typeof document !== "undefined" && document.visibilityState === "visible" ? "visible" : "hidden";
|
|
1331
|
+
fetch(`${FOCUS_PATH}?vis=${vis}`, { method: "POST" }).catch(() => {});
|
|
1327
1332
|
} catch {}
|
|
1328
1333
|
// `window.focus()` only focuses the page, not the OS
|
|
1329
1334
|
// window. The route above restores and focuses the real
|
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.4",
|
|
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",
|