dsh-plugin-notify-sounds 1.9.0 → 1.9.2
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 -6
- package/README.zh.md +8 -6
- package/lib/client.js +207 -17
- package/lib/index.js +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Walk away from the window without missing anything: an OS notification in the ba
|
|
|
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.2
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -35,7 +35,7 @@ Every visual detail of the settings section comes from DSH's own design system:
|
|
|
35
35
|
| **Missed it? It reminds again** | Unanswered decisions re-ping every 1/5/15/30 minutes — and **stop for good once you have seen them**, even if you switch away again |
|
|
36
36
|
| **Only what you care about** | Sub-agents (background tasks) are completely silent by default — only your own conversation is announced |
|
|
37
37
|
| **Quiet when it should be** | Quiet hours (e.g. 22:00–08:00, wrapping past midnight), notify-only-when-hidden, and a master switch |
|
|
38
|
-
| **Your sound, your way** | Nine built-in sounds; **up to three of your own clips**,
|
|
38
|
+
| **Your sound, your way** | Nine built-in sounds, levelled to each other; **up to three of your own clips**, named and assignable to any alert (mp3 / wav / ogg / m4a, up to 15s each); volume — each feature with its own toggle |
|
|
39
39
|
| **Update checks** | Checks npm for a newer version and points at the route that installs it — the **Plugins** panel in the sidebar (uninstall, then add again), the marketplace, or `dsh plugin add` on a web profile; the row also flags an install the profile's lockfile does not match |
|
|
40
40
|
|
|
41
41
|
## 💡 Why
|
|
@@ -48,7 +48,7 @@ This plugin turns "it is waiting for you" and "it is done" into sound. With syst
|
|
|
48
48
|
|
|
49
49
|
Bell · Chime · Ding · Drop · Pulse · Arc · Coin · Success · Knock — nine built-in sounds, all synthesized live with Web Audio: no audio files, no network requests.
|
|
50
50
|
|
|
51
|
-
Or upload your own
|
|
51
|
+
Or upload your own audio (mp3 / wav / ogg / m4a, up to 15s and 3MB each, validated and loudness-normalized), or pick "None" for notifications only. Each trigger (approval / question / completion) gets its own sound, with preview right in the settings.
|
|
52
52
|
|
|
53
53
|
## ⚙️ Settings
|
|
54
54
|
|
|
@@ -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.2
|
|
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.2.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.)
|
|
@@ -145,10 +145,12 @@ For anyone hacking on the code (exact details live in the source comments):
|
|
|
145
145
|
|
|
146
146
|
## 📜 Changelog
|
|
147
147
|
|
|
148
|
+
- **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)
|
|
149
|
+
- **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)
|
|
148
150
|
- **v1.9.0** (2026-10-27) **Your own sounds become a small library: three at most, each renameable and assignable.** All three alerts used to share a single "Custom" upload, so giving "finished" its own sound while "approval needed" kept a preset was impossible. The "Custom sound" row now holds everything: press **Add audio**, and the file is listed underneath (three at most) where it can be renamed, previewed or removed; the three alert pickers list your sounds by name, so each alert plays the one you point it at. **The bytes moved out of the config**: a 512KB clip used to become 699,052 characters of base64 inside `cordis.patch.yml` — 277× the whole file, rewritten on every settings change — so audio is now stored under `profiles/<profile>/notify-sounds/sounds/<hash>.<ext>` with only the name in the config. Three gaps closed as well: **3MB and 15 seconds per sound** (a long clip turns an alert into background music and can cost tens of MB once decoded); **silent and near-zero-length files are refused at upload**; and **anything unreadable falls back to that alert's default sound** — a damaged payload used to silence all three with nothing in the UI to explain it. The list marks a file that has gone missing, and one no alert uses yet. Existing audio from an older release is moved into the library on first run; if that fails it stays where it was and keeps playing.
|
|
149
151
|
- **v1.8.6** (2026-10-23) **A full audit, paid off — no new features.** Fixed: with goal-only alerting on, turning the switch **off** while the watcher waited still produced the chime it was holding at the end of the goal (the watcher now re-reads the preference); the update check cached a FAILURE for the full day, so one network blip left the row reading "check failed" until tomorrow (failures are now cached for a minute, and the row no longer blames the network — it says neither source returned a usable answer); a `pnpm-lock.yaml` key carrying a peer suffix (`pkg@1.8.4(@scope/peer@3.18.4)`) was read as a version, which flagged a perfectly good install as unregistered and told the user to reinstall it; an unreadable current version rendered as "This version ? · Up to date", which is now its own message; the repeat-interval control displayed a hand-edited 7 as "5 min" while reminders fired every seven (display and timer now share one snap); identical quiet-hours bounds were silently treated as "no quiet" and the description now says so; the client's time parser accepted `9:05` while the Host schema rejected it (both are strict now); a custom-sound read failure was reported as "could not decode"; and one refused notification construction left the row permanently "unsupported" (a successful one now clears it). **Update guidance**: on the Desktop the plugin manager is a **sidebar panel named Plugins**, not "Settings → Plugins" — 1.8.5 got that wrong; every install command now pins the version (it rendered as `@v1.8.5`, and the display prefix does not belong in a command a package manager parses), and the copy explains that a release younger than pnpm's `minimumReleaseAge` window resolves back to the previous one. **Cleanup**: removed the 527-line unreachable self-installer from 1.8.0 (the Host file drops from 1553 to 1023 lines, along with 10 imports that existed only for it), the update notice that could never render, eight dead dictionary keys, and the tests that pinned the dead code; all three route paths are now cross-checked between the halves (`/notify-sounds/goal` was missing, and it is the one that has already drifted once). The README's "multiple windows fire independently" claim contradicted the speaking-page election shipped in 1.7.0 and is corrected.
|
|
150
152
|
- **v1.8.5** (2026-10-20) **The wording catches up with the mechanism.** The update row still said a newer version could be installed "in one click" — it cannot since 1.8.0: the row only checks and points at the package manager. The row now says exactly that, and its in-app guidance carries the version (`dsh-plugin-notify-sounds@{version}`) so a fresh release is not silently resolved to the previous one by pnpm's `minimumReleaseAge` policy. The README feature table, settings summary and install step no longer promise one-click installs; historical changelog entries are left untouched.
|
|
151
|
-
- **v1.8.4** (2026-10-20) **A click always finds the window.** A trace of a real click showed the native raise giving up with "no window found" while the window existed: a hidden or repainting window can transiently answer with an empty title, and the search had nothing else to match on. The window search now also matches the process that owns the window (the Desktop shell is this Host's parent), and the foreground request is retried three times before the deep-link fallback is paid for.
|
|
153
|
+
- **v1.8.4** (2026-10-20) **A click always finds the window.** A trace of a real click showed the native raise giving up with "no window found" while the window existed: a hidden or repainting window can transiently answer with an empty title, and the search had nothing else to match on. The window search now also matches the process that owns the window (the Desktop shell is this Host's parent), and the foreground request is retried three times before the deep-link fallback is paid for. ([#2](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/2), thanks @hxwd94666)
|
|
152
154
|
- **v1.8.3** (2026-10-20) **Click-to-return works when the window is closed to the tray.** The Win32 raise searched only *visible* windows, so a DSH window hidden to the tray was never found and the click fell back to the slow deep-link route — which reads as "clicked the notification, nothing happened". The search now also remembers an invisible window whose title matches, and the raise shows it (`SW_SHOW`) before restoring and focusing it; the deep link stays as the last resort.
|
|
153
155
|
- **v1.8.2** (2026-10-20) **Actually fixes goal-mode alerting.** The 1.8.1 host route read `stateOf(session, "goal")` but took `current.phase`, while the projection value nests the goal: `current.goal.phase`. Every read therefore came back empty and every round chimed. The route now reads the nested field (with a fallback for a flattened shape), and a Host-side debug trace confirmed `current.goal.phase` is `active` on intermediate rounds and `complete` on the final one.
|
|
154
156
|
- **v1.8.1** (2026-10-20) **Fixes goal-mode alerting.** The 1.8.0 switch read the phase from the browser's own session projection, which only carries sessions that page has opened — for a goal in a session it never opened the read came back empty, which the code treated as "no goal", so every round chimed again. The browser half now asks the Host's new read-only `/notify-sounds/goal` route, whose projection store is authoritative, and re-checks it while the goal is still `active` (the single chime lands when the goal completes, pauses or blocks; a session that starts running again stays silent). A Host that does not answer the route keeps the old behaviour.
|
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.2
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -35,7 +35,7 @@ dsh-plugin-notify-sounds@1.9.0
|
|
|
35
35
|
| **没看到会再提醒** | 未处理决策按 1/5/15/30 分钟重复提醒 |
|
|
36
36
|
| **只提醒你关心的** | 子智能体(后台任务)默认完全静默,只提醒主对话,可开关 |
|
|
37
37
|
| **该安静时安静** | 静音时段(如 22:00–08:00,可跨午夜)、仅页面不可见时提示、总开关一键全静音 |
|
|
38
|
-
| **声音随你** |
|
|
38
|
+
| **声音随你** | 九种内置音效(彼此响度已对齐);**最多 3 个自己的音频**,可改名、指派给任意提醒(mp3 / wav / ogg / m4a,每个 ≤15 秒);音量调节;每项独立开关 |
|
|
39
39
|
| **检测更新** | 设置里检查 npm 新版本,并指向真正负责安装的入口——**左侧栏「插件」**(卸载后再装)、市场,或 web profile 上的 `dsh plugin add`;若当前安装未被 lockfile 记录,会在行内提示 |
|
|
40
40
|
|
|
41
41
|
## 💡 为什么需要它
|
|
@@ -48,7 +48,7 @@ DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别
|
|
|
48
48
|
|
|
49
49
|
双音铃 · 风铃 · 单音叮 · 水滴 · 脉冲 · 上行琶音 · 金币 · 达成 · 敲门 —— 九种内置音效全部由 Web Audio 现场合成,不含任何音频文件、不发一次网络请求。
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
也可以换上你自己的音频(mp3 / wav / ogg / m4a,每个 ≤15 秒、3MB,自动校验并做响度归一化),或者干脆选「无声」只留通知。三种触发(审批 / 提问 / 完成)各选各的音效,设置里随时试听。
|
|
52
52
|
|
|
53
53
|
## ⚙️ 设置
|
|
54
54
|
|
|
@@ -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.2
|
|
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.2.tgz
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
(`dsh plugin add` 把参数原样交给包管理器,所以要给本地文件路径,不能填下载地址。)
|
|
@@ -144,10 +144,12 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
|
144
144
|
|
|
145
145
|
## 📜 更新记录
|
|
146
146
|
|
|
147
|
+
- **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)
|
|
148
|
+
- **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)
|
|
147
149
|
- **v1.9.0**(2026-10-27)**自定义音频变成一个小库:最多 3 个,可以改名、指派。** 此前三个提醒共用同一个「自定义」上传——想给「完成」配一个、同时让「决策」用内置音,做不到。现在「自定义音效」只有一行:点「添加音频」选文件,加进去的会列在它下面(最多 3 个),每个都能改名、试听、移除;三个提醒的音效下拉里会直接列出这些音频的名字,挑哪个就响哪个。**存放位置也换了**:音频不再以 base64 塞进 `cordis.patch.yml`(一个 512KB 的音频就是 699,052 个字符,是整份配置的 277 倍,而且每次改设置都要整份重新广播),改为存到 `profiles/<profile>/notify-sounds/sounds/<哈希>.<后缀>`,配置里只留名字。顺带补了三件一直缺的:**上限 3MB、每个 15 秒**(太长的音频会变成背景音,解码后还可能占几十 MB 内存);**没有声音、或短到听不见的文件,在上传时就被拒绝**;**读不到音频时改用该项的默认音**——此前配置里的音频一旦损坏,三个提醒会全部静默,界面上还什么都不说。列表里也会标出「文件找不到了」和「还没有提醒使用它」。老用户升级时,原来的音频会自动搬进这个库;搬不进去就原样留着,照旧能响。
|
|
148
150
|
- **v1.8.6**(2026-10-23)**把一次全量审计翻出来的问题收干净,没有新功能。** 修复:目标模式下,若用户在等到目标结束之前关掉「只在最终轮提醒」,目标结束后仍会补响一次——等待器现在会复查这个开关;更新检查把「失败」也缓存满 24 小时,一次网络抖动就让整行显示一整天「检查失败」,现在失败只缓存 1 分钟,并且不再断言原因是网络,而是如实说两个源都没给出可用答复;`pnpm-lock.yaml` 里带 peer 后缀的键(`pkg@1.8.4(@scope/peer@3.18.4)`)会被当成版本号,让正常的安装被误报「未被包管理器登记」并劝用户重装;当前版本读不出来时,界面会显示「当前版本 ? · 已是最新」,现在单独说明无法读取;「重复提醒间隔」把手改的 7 分钟显示成 5 分钟、实际按 7 分钟响,现在显示与计时共用同一次吸附;静音时段起止相同被当作「不静音」,描述已写明;客户端的时间解析接受 `9:05` 而宿主 schema 拒绝,现在两边一样严;自定义音效读取文件失败时误报「无法解码」,现在分开说;系统通知构造失败一次后整页永久显示「不支持」,现在成功一次即恢复。**更新指引**:桌面端的插件管理器是**左侧栏「插件」**(不是「设置 → 插件」),1.8.5 写错了,已改正;安装命令一律带版本号(此前渲染成 `@v1.8.5`,多了一个 `v`,复制进包管理器未必能解析),并说明发布不满 24 小时的版本会被 pnpm 的 `minimumReleaseAge` 挡掉。**清理**:移除 1.8.0 留下的 527 行不可达自安装器(宿主文件从 1553 行降到 1023 行,连同 10 个只服务于它的导入)、永远返回 null 的更新通知组件、8 个死字典键,以及只测死代码的测试;三条路由的拼写现在都在跨半契约测试里对齐(`/notify-sounds/goal` 此前漏了,而它正是漂移过一次的那条)。README 里「多窗口各自响一次」的说法与 1.7.0 起的发声页选举矛盾,已改正。
|
|
149
151
|
- **v1.8.5**(2026-10-20)**文案跟上机制。** 更新行此前仍写着「发现新版可一键安装」——从 1.8.0 起已不支持:这一行只负责检查,并指向包管理器。现在行内如实说明,且引导里带上版本号(`dsh-plugin-notify-sounds@{version}`),避免刚发布的版本被 pnpm 的 `minimumReleaseAge` 策略静默解析成上一个版本。README 的功能表、设置摘要与安装步骤不再承诺一键安装;历史更新记录保持原样。
|
|
150
|
-
- **v1.8.4**(2026-10-20)**点击通知一定能找到窗口。** 真实点击的日志显示:窗口明明存在,原生唤窗却报 "no window found"
|
|
152
|
+
- **v1.8.4**(2026-10-20)**点击通知一定能找到窗口。** 真实点击的日志显示:窗口明明存在,原生唤窗却报 "no window found"——隐藏或正在重绘的窗口可能瞬时返回空标题,而当时的搜索只认标题。现在窗口搜索同时按「拥有窗口的进程」匹配(桌面壳是本宿主进程的父进程),前台请求连续重试三次之后才走深链兜底。([#2](https://github.com/BillCx330/dsh-plugin-notify-sounds/issues/2),感谢 @hxwd94666)
|
|
151
153
|
- **v1.8.3**(2026-10-20)**窗口关到托盘时,点击通知也能回到 DSH 了。** 原先的 Win32 唤窗只搜**可见**窗口,关到托盘的 DSH 窗口永远找不到,只能退到很慢的深链兜底——表现就是「点了通知没反应」。现在会记住标题匹配的不可见窗口,并在置前之前先 `SW_SHOW` 让它显示出来;深链仍作为最后兜底。
|
|
152
154
|
- **v1.8.2**(2026-10-20)**真正修好目标模式的提醒。** 1.8.1 的宿主路由读了 `stateOf(session, "goal")`,却取 `current.phase`,而投影值把 goal 嵌在里面:`current.goal.phase`。于是每次读取都为空、每轮照响。现在改为读嵌套字段(并保留扁平形状的兜底),宿主侧调试记录已确认:中间轮 `current.goal.phase` 是 `active`,最后一轮是 `complete`。
|
|
153
155
|
- **v1.8.1**(2026-10-20)**修复目标模式的提醒。** 1.8.0 的开关从浏览器自己的会话投影读 phase,而那份投影只包含这个页面打开过的会话——目标所在的会话没被打开过时读到空值,代码把它当成「没有目标」,于是每轮又照响。浏览器半现在改问宿主新增的只读路由 `/notify-sounds/goal`(宿主的投影存储才是权威的),并在目标仍为 `active` 时持续复查:目标完成、暂停或受阻时响唯一一次;会话重新开始运行则保持安静。宿主不回答该路由时退回旧行为。
|
package/lib/client.js
CHANGED
|
@@ -206,7 +206,7 @@ window.__ModuleLoader__.load({
|
|
|
206
206
|
"custom.unreadable": "读不到这个文件:它可能已被移动,或没有读取权限,请重新选择",
|
|
207
207
|
"custom.missing": "音频文件找不到了(可能被删除或被杀毒软件隔离),会改用默认音。请重新添加",
|
|
208
208
|
"systemNotify.title": "系统通知",
|
|
209
|
-
"systemNotify.description": "
|
|
209
|
+
"systemNotify.description": "页面处于后台时弹出系统通知,标题用当前对话的名字,点击可回到窗口",
|
|
210
210
|
"systemNotify.granted": "已授权系统通知",
|
|
211
211
|
"systemNotify.denied": "通知权限被拒绝,请在浏览器或系统设置中允许后重试",
|
|
212
212
|
"systemNotify.unsupported": "当前环境不支持系统通知",
|
|
@@ -239,6 +239,7 @@ window.__ModuleLoader__.load({
|
|
|
239
239
|
"sound.custom": "自定义",
|
|
240
240
|
"notify.decision.title": "需要你的决策",
|
|
241
241
|
"notify.decision.body": "智能体正在等待你的决定",
|
|
242
|
+
"notify.decision.event": "需要你的决策:{detail}",
|
|
242
243
|
"notify.completion.title": "任务完成",
|
|
243
244
|
"notify.completion.body": "智能体已完成当前任务",
|
|
244
245
|
"update.title": "更新",
|
|
@@ -298,7 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
298
299
|
"custom.unreadable": "Could not read this file: it may have been moved, or permission was revoked — choose it again",
|
|
299
300
|
"custom.missing": "The audio file is missing (deleted, or quarantined by antivirus), so the default sound plays instead. Add it again",
|
|
300
301
|
"systemNotify.title": "System notifications",
|
|
301
|
-
"systemNotify.description": "Show an OS notification while the page is hidden; click it to focus the window",
|
|
302
|
+
"systemNotify.description": "Show an OS notification while the page is hidden — titled with the conversation name; click it to focus the window",
|
|
302
303
|
"systemNotify.granted": "Notification permission granted",
|
|
303
304
|
"systemNotify.denied": "Denied — allow notifications in the browser or system settings and retry",
|
|
304
305
|
"systemNotify.unsupported": "System notifications are unavailable in this environment",
|
|
@@ -333,6 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
333
334
|
"sound.custom": "Custom",
|
|
334
335
|
"notify.decision.title": "Your decision is needed",
|
|
335
336
|
"notify.decision.body": "The agent is waiting for your decision",
|
|
337
|
+
"notify.decision.event": "Your decision is needed: {detail}",
|
|
336
338
|
"notify.completion.title": "Task finished",
|
|
337
339
|
"notify.completion.body": "The agent has finished the current task",
|
|
338
340
|
"update.title": "Updates",
|
|
@@ -456,11 +458,14 @@ window.__ModuleLoader__.load({
|
|
|
456
458
|
/** Bumped when a preview starts, so a decode still in flight knows it lost. */
|
|
457
459
|
let previewToken = 0;
|
|
458
460
|
|
|
459
|
-
/** Remember one scheduled source and forget it once it ends. */
|
|
460
|
-
function trackSource(source) {
|
|
461
|
+
/** Remember one scheduled source, run its cleanup, and forget it once it ends. */
|
|
462
|
+
function trackSource(source, cleanup) {
|
|
461
463
|
liveSources.add(source);
|
|
462
464
|
try {
|
|
463
|
-
source.onended = () =>
|
|
465
|
+
source.onended = () => {
|
|
466
|
+
liveSources.delete(source);
|
|
467
|
+
cleanup?.();
|
|
468
|
+
};
|
|
464
469
|
} catch {
|
|
465
470
|
// A source that refuses the handler still plays; it just stays
|
|
466
471
|
// tracked until the next stop clears the set.
|
|
@@ -488,6 +493,90 @@ window.__ModuleLoader__.load({
|
|
|
488
493
|
liveSources.clear();
|
|
489
494
|
}
|
|
490
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Peak of one preset's summed voices, measured once at load.
|
|
498
|
+
*
|
|
499
|
+
* The presets were written with `note.gain * 0.25` per voice and nothing
|
|
500
|
+
* held them to each other: `coin` peaked at 0.105 while `ding` reached
|
|
501
|
+
* 0.315, a 9.5 dB spread, and every one of them sat far below a decoded
|
|
502
|
+
* clip (whose peak is normalized to 1). At 100% a preset played at
|
|
503
|
+
* -16 dBFS while the user's own file played at -4 — the same slider
|
|
504
|
+
* value was two different loudnesses.
|
|
505
|
+
*
|
|
506
|
+
* This walks each preset's envelope at 2 ms resolution and returns the
|
|
507
|
+
* loudest sum, so every voice can be scaled to one target instead.
|
|
508
|
+
*
|
|
509
|
+
* @param preset - one entry of SOUND_PRESETS.
|
|
510
|
+
* @returns the peak the summed envelopes reach, before any level.
|
|
511
|
+
*/
|
|
512
|
+
function presetPeak(preset) {
|
|
513
|
+
const ATTACK = 0.012;
|
|
514
|
+
const value = (note, t) => {
|
|
515
|
+
if (t < note.at) return 0;
|
|
516
|
+
const peak = note.gain * 0.25;
|
|
517
|
+
const attackEnd = note.at + ATTACK;
|
|
518
|
+
if (t <= attackEnd) {
|
|
519
|
+
// exponentialRampToValueAtTime from 0.0001, same as playback
|
|
520
|
+
return 0.0001 * Math.pow(peak / 0.0001, (t - note.at) / ATTACK);
|
|
521
|
+
}
|
|
522
|
+
const end = note.at + note.dur;
|
|
523
|
+
if (t >= end) return 0;
|
|
524
|
+
return peak * Math.pow(0.0001 / peak, (t - attackEnd) / (end - attackEnd));
|
|
525
|
+
};
|
|
526
|
+
let peak = 0;
|
|
527
|
+
for (let t = 0; t < 2; t += 0.002) {
|
|
528
|
+
let sum = 0;
|
|
529
|
+
for (const note of preset.notes) sum += value(note, t);
|
|
530
|
+
if (sum > peak) peak = sum;
|
|
531
|
+
}
|
|
532
|
+
return peak;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* RMS-to-peak ratio of each waveform, at the frequencies these presets use.
|
|
537
|
+
*
|
|
538
|
+
* A sine peaks at √2 times its RMS; a square's RMS is its peak. So at
|
|
539
|
+
* the same peak a square carries 3 dB more energy — which is exactly why
|
|
540
|
+
* `coin` (two square notes) sounded louder than `ding` (two sines) once
|
|
541
|
+
* both were normalized to one peak. Triangle sits below the sine, but it
|
|
542
|
+
* is not scaled up: that would push its peak through the ceiling.
|
|
543
|
+
*/
|
|
544
|
+
const WAVEFORM_RMS = { sine: Math.SQRT1_2, triangle: Math.sqrt(1 / 3), square: 1 };
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* One level per preset, chosen so every preset lands at the same
|
|
548
|
+
* loudness rather than the same peak.
|
|
549
|
+
*
|
|
550
|
+
* The headline number is still a peak ceiling (0.95), because that is
|
|
551
|
+
* what keeps the output from clipping; the waveform's own RMS ratio is
|
|
552
|
+
* then folded in as a crest-factor correction. Anything quieter per peak
|
|
553
|
+
* (triangle) is capped at the peak target instead of being pushed past
|
|
554
|
+
* it, so the correction only ever pulls a preset down.
|
|
555
|
+
*/
|
|
556
|
+
const PRESET_LEVEL = Object.fromEntries(
|
|
557
|
+
Object.entries(SOUND_PRESETS).map(([id, preset]) => {
|
|
558
|
+
const crest = Math.max(
|
|
559
|
+
...preset.notes.map((note) => (WAVEFORM_RMS[note.type ?? "sine"] ?? Math.SQRT1_2) / Math.SQRT1_2),
|
|
560
|
+
);
|
|
561
|
+
return [id, 0.95 / (presetPeak(preset) * Math.max(1, crest))];
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Build the output end of one playback.
|
|
567
|
+
*
|
|
568
|
+
* @param engine - the page's AudioContext.
|
|
569
|
+
* @param loudness - 0–1, already clamped by the caller.
|
|
570
|
+
* @param level - the preset's own leveling factor (1 for clips).
|
|
571
|
+
* @returns the node the sources connect to, and every node to release.
|
|
572
|
+
*/
|
|
573
|
+
function outputChain(engine, loudness, level) {
|
|
574
|
+
const master = engine.createGain();
|
|
575
|
+
master.gain.value = loudness * 0.6 * level;
|
|
576
|
+
master.connect(engine.destination);
|
|
577
|
+
return { master, nodes: [master] };
|
|
578
|
+
}
|
|
579
|
+
|
|
491
580
|
/** Play one decoded buffer at a loudness in percent, through a peak-normalized gain. */
|
|
492
581
|
function playBuffer(engine, buffer, normGain, volumePercent) {
|
|
493
582
|
const percent = Number.isFinite(volumePercent) ? volumePercent : DEFAULT_VOLUME;
|
|
@@ -496,10 +585,19 @@ window.__ModuleLoader__.load({
|
|
|
496
585
|
const source = engine.createBufferSource();
|
|
497
586
|
const gain = engine.createGain();
|
|
498
587
|
source.buffer = buffer;
|
|
499
|
-
gain.gain.value = normGain
|
|
588
|
+
gain.gain.value = normGain;
|
|
589
|
+
const { master, nodes } = outputChain(engine, loudness, 1);
|
|
500
590
|
source.connect(gain);
|
|
501
|
-
gain.connect(
|
|
502
|
-
|
|
591
|
+
gain.connect(master);
|
|
592
|
+
// A clip's nodes outlive its buffer unless something disconnects
|
|
593
|
+
// them; the Web Audio graph keeps every connected node alive.
|
|
594
|
+
trackSource(source, () => {
|
|
595
|
+
for (const node of [gain, ...nodes]) {
|
|
596
|
+
try {
|
|
597
|
+
node.disconnect();
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
503
601
|
source.start(engine.currentTime + 0.02);
|
|
504
602
|
}
|
|
505
603
|
|
|
@@ -701,9 +799,9 @@ window.__ModuleLoader__.load({
|
|
|
701
799
|
const loudness = Math.max(0, Math.min(100, percent)) / 100;
|
|
702
800
|
if (loudness === 0) return;
|
|
703
801
|
whenRunning(engine, () => {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
master
|
|
802
|
+
// Every preset is scaled to the same peak, so a slider value
|
|
803
|
+
// means one loudness whichever sound is selected.
|
|
804
|
+
const { master, nodes } = outputChain(engine, loudness, PRESET_LEVEL[id] ?? 1);
|
|
707
805
|
const t0 = engine.currentTime + 0.02;
|
|
708
806
|
let tail = 0;
|
|
709
807
|
for (const note of preset.notes) {
|
|
@@ -743,7 +841,7 @@ window.__ModuleLoader__.load({
|
|
|
743
841
|
setTimeout(() => release(true), remaining() + 50);
|
|
744
842
|
return;
|
|
745
843
|
}
|
|
746
|
-
|
|
844
|
+
for (const node of nodes) node.disconnect();
|
|
747
845
|
} catch {}
|
|
748
846
|
};
|
|
749
847
|
const timer = setTimeout(() => release(false), Math.max(0, remaining() + 50));
|
|
@@ -1246,6 +1344,50 @@ window.__ModuleLoader__.load({
|
|
|
1246
1344
|
*/
|
|
1247
1345
|
let notificationConstructionFailed = false;
|
|
1248
1346
|
|
|
1347
|
+
/** Longest conversation title a toast shows before it is trimmed. */
|
|
1348
|
+
const NOTIFICATION_TITLE_MAX = 48;
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* The conversation's own name, for a notification title.
|
|
1352
|
+
*
|
|
1353
|
+
* Every DSH release since 0.2.0 puts both fields on the session row we
|
|
1354
|
+
* already read: `title` is the persisted name (empty until one is set),
|
|
1355
|
+
* `displayTitle` falls back to the workspace folder and then the raw id.
|
|
1356
|
+
* A toast that says only "任务完成" makes the user open the window to
|
|
1357
|
+
* learn WHICH conversation finished; this is the name they already know
|
|
1358
|
+
* from the sidebar.
|
|
1359
|
+
*
|
|
1360
|
+
* Falls back to "" — the caller then keeps its own event title — so a
|
|
1361
|
+
* runtime that renames or drops these fields loses the nicety rather
|
|
1362
|
+
* than printing a raw id.
|
|
1363
|
+
*
|
|
1364
|
+
* @param sessionId - the session the alert is about.
|
|
1365
|
+
* @param rows - the pass's session rows, when the runtime has them.
|
|
1366
|
+
* @returns the name to show, or "" when there is none.
|
|
1367
|
+
*/
|
|
1368
|
+
function conversationTitle(sessionId, rows) {
|
|
1369
|
+
const row = rows?.[sessionId];
|
|
1370
|
+
if (row === null || typeof row !== "object") return "";
|
|
1371
|
+
const named = typeof row.title === "string" ? row.title.trim() : "";
|
|
1372
|
+
const cwd = typeof row.cwd === "string" ? row.cwd : "";
|
|
1373
|
+
const shown = typeof row.displayTitle === "string" ? row.displayTitle.trim() : "";
|
|
1374
|
+
// DSH's own fallback names a session after its working directory
|
|
1375
|
+
// ("Working directory: ..."). Some sessions keep that string as their
|
|
1376
|
+
// title; it is 40+ characters of path that says no more than the folder
|
|
1377
|
+
// name and puts the path on the lock screen, so it counts as unnamed —
|
|
1378
|
+
// the folder's last segment is the useful part.
|
|
1379
|
+
const lower = named.toLowerCase();
|
|
1380
|
+
const restatesCwd = named === "" || named === cwd || lower.startsWith("working directory:");
|
|
1381
|
+
const folder = cwd === "" ? "" : cwd.split(/[\\/]/u).filter(Boolean).pop() ?? "";
|
|
1382
|
+
// Prefer the name the sidebar shows, but never hand back the same
|
|
1383
|
+
// restatement we just rejected; the folder alone is shorter and says
|
|
1384
|
+
// the same thing.
|
|
1385
|
+
const fallback = shown !== "" && shown !== sessionId && shown !== named ? shown : folder;
|
|
1386
|
+
const name = restatesCwd ? fallback : named;
|
|
1387
|
+
if (name === "") return "";
|
|
1388
|
+
return name.length > NOTIFICATION_TITLE_MAX ? `${name.slice(0, NOTIFICATION_TITLE_MAX - 1)}…` : name;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1249
1391
|
/**
|
|
1250
1392
|
* Human-readable summary of one pending interaction, for notification
|
|
1251
1393
|
* bodies: the approval's localized reason first, then its raw reason,
|
|
@@ -2618,6 +2760,42 @@ window.__ModuleLoader__.load({
|
|
|
2618
2760
|
}
|
|
2619
2761
|
};
|
|
2620
2762
|
|
|
2763
|
+
/**
|
|
2764
|
+
* Re-scale the stored volume once, for the release that changed what
|
|
2765
|
+
* the number means.
|
|
2766
|
+
*
|
|
2767
|
+
* 1.9.1 made every preset about 10 dB louder at the same setting. A
|
|
2768
|
+
* user who left the slider at 60 would hear a jump they never asked
|
|
2769
|
+
* for, so the stored value is halved: at half the number the new
|
|
2770
|
+
* scale lands about 3.5 dB above where they were — noticeable, not
|
|
2771
|
+
* startling — and the range above it is free for anyone who wants
|
|
2772
|
+
* the new headroom.
|
|
2773
|
+
*
|
|
2774
|
+
* The marker is written BEFORE the volume, deliberately. If the
|
|
2775
|
+
* second write is refused the user keeps their old (loud) setting
|
|
2776
|
+
* and can turn it down; the reverse order would halve the number
|
|
2777
|
+
* again on the next launch.
|
|
2778
|
+
*/
|
|
2779
|
+
let volumeMigrating = false;
|
|
2780
|
+
const migrateVolume = async () => {
|
|
2781
|
+
if (volumeMigrating) return;
|
|
2782
|
+
const value = form.getSnapshot()?.value;
|
|
2783
|
+
if (value === undefined || value === null) return;
|
|
2784
|
+
if (value.volumeRev === 1) return;
|
|
2785
|
+
volumeMigrating = true;
|
|
2786
|
+
const current = Number.isFinite(value?.volume) ? value.volume : 60;
|
|
2787
|
+
const scaled = Math.max(5, Math.min(100, Math.round(current / 2)));
|
|
2788
|
+
try {
|
|
2789
|
+
const marked = await form.set("volumeRev", 1);
|
|
2790
|
+
if (marked !== true) return;
|
|
2791
|
+
await form.set("volume", scaled);
|
|
2792
|
+
} catch {
|
|
2793
|
+
// Nothing to undo: the marker only says the scale is current.
|
|
2794
|
+
} finally {
|
|
2795
|
+
volumeMigrating = false;
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
|
|
2621
2799
|
/**
|
|
2622
2800
|
* Bring sounds from an older release into the library.
|
|
2623
2801
|
*
|
|
@@ -2698,9 +2876,11 @@ window.__ModuleLoader__.load({
|
|
|
2698
2876
|
ctx.effect(() => {
|
|
2699
2877
|
let running = false;
|
|
2700
2878
|
const attempt = () => {
|
|
2879
|
+
const current = form.getSnapshot()?.value;
|
|
2880
|
+
if (current === undefined || current === null) return;
|
|
2881
|
+
if (current.volumeRev !== 1) void migrateVolume();
|
|
2701
2882
|
if (running) return;
|
|
2702
|
-
const value =
|
|
2703
|
-
if (value === undefined || value === null) return;
|
|
2883
|
+
const value = current;
|
|
2704
2884
|
const hasInline = typeof value.customSound === "string" && value.customSound !== "";
|
|
2705
2885
|
const hasPerTrigger = ["Decision", "Question", "Completion"].some((suffix) => typeof value[`custom${suffix}File`] === "string" && value[`custom${suffix}File`] !== "");
|
|
2706
2886
|
if (!hasInline && !hasPerTrigger) return;
|
|
@@ -2852,11 +3032,19 @@ window.__ModuleLoader__.load({
|
|
|
2852
3032
|
// the legacy inline payload, or the preset its picker names.
|
|
2853
3033
|
playAlert(kind, value, volume);
|
|
2854
3034
|
if (value?.systemNotify === true && typeof document !== "undefined" && document.visibilityState !== "visible") {
|
|
3035
|
+
// The name the user knows this conversation by, when the
|
|
3036
|
+
// runtime exposes one. Naming it only helps if the toast
|
|
3037
|
+
// still says what happened, so for a decision the event
|
|
3038
|
+
// moves into the body instead of being dropped.
|
|
3039
|
+
const conversation = conversationTitle(sessionId, readSessionRows());
|
|
3040
|
+
const eventTitle = t(kind === "completion" ? "notify.completion.title" : "notify.decision.title");
|
|
3041
|
+
const title = conversation !== "" ? conversation : eventTitle;
|
|
2855
3042
|
if (kind === "completion") {
|
|
2856
|
-
showSystemNotification(
|
|
3043
|
+
showSystemNotification(title, t("notify.completion.body"), "dsh-notify-sounds:completion");
|
|
2857
3044
|
} else {
|
|
2858
|
-
const
|
|
2859
|
-
|
|
3045
|
+
const detail = interactionSummary(interaction, resolvePackageText) ?? t("notify.decision.body");
|
|
3046
|
+
const body = conversation !== "" ? t("notify.decision.event", { detail }) : detail;
|
|
3047
|
+
showSystemNotification(title, body, "dsh-notify-sounds:decision");
|
|
2860
3048
|
}
|
|
2861
3049
|
}
|
|
2862
3050
|
return true;
|
|
@@ -3267,6 +3455,8 @@ window.__ModuleLoader__.load({
|
|
|
3267
3455
|
exports.GOAL_PHASE_PATH = GOAL_PHASE_PATH;
|
|
3268
3456
|
exports.SOUND_PATH = SOUND_ROUTE;
|
|
3269
3457
|
exports.SOUND_MAX_BYTES = SOUND_MAX_BYTES;
|
|
3458
|
+
exports.PRESET_LEVEL = PRESET_LEVEL;
|
|
3459
|
+
exports.SOUND_PRESETS = SOUND_PRESETS;
|
|
3270
3460
|
// The alert election, so the tests can drive two pages against a stubbed
|
|
3271
3461
|
// storage and channel instead of hoping two real tabs cooperate.
|
|
3272
3462
|
exports.createAlertLeader = createAlertLeader;
|
package/lib/index.js
CHANGED
|
@@ -783,7 +783,20 @@ export function writeSoundFile(bytes) {
|
|
|
783
783
|
mkdirSync(directory, { recursive: true });
|
|
784
784
|
const temporary = `${target}.tmp`;
|
|
785
785
|
writeFileSync(temporary, buffer);
|
|
786
|
-
|
|
786
|
+
try {
|
|
787
|
+
renameSync(temporary, target);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
// POSIX renames straight over an existing file; Windows refuses with
|
|
790
|
+
// EPERM/EEXIST. Two windows uploading the same bytes race here: the
|
|
791
|
+
// other one already wrote this exact content-addressed file, so a
|
|
792
|
+
// refusal is the outcome we wanted, not a failure.
|
|
793
|
+
if (existsSync(target)) {
|
|
794
|
+
rmSync(temporary, { force: true });
|
|
795
|
+
return fileName;
|
|
796
|
+
}
|
|
797
|
+
rmSync(temporary, { force: true });
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
787
800
|
return fileName;
|
|
788
801
|
} catch (error) {
|
|
789
802
|
// A read-only profile or a full disk must not take the route down: the
|
|
@@ -1039,6 +1052,14 @@ export const Config = z.object({
|
|
|
1039
1052
|
enabled: z.boolean().default(true).volatile(),
|
|
1040
1053
|
/** Playback loudness in percent. */
|
|
1041
1054
|
volume: z.number().step(1).min(0).max(100).default(60).volatile(),
|
|
1055
|
+
/**
|
|
1056
|
+
* Which loudness scale the stored `volume` speaks.
|
|
1057
|
+
*
|
|
1058
|
+
* 1.9.1 made every preset ~10 dB louder at the same number, so the page
|
|
1059
|
+
* re-scales a stored value once and sets this to 1. A config without it has
|
|
1060
|
+
* never been re-scaled, including one written by an older release.
|
|
1061
|
+
*/
|
|
1062
|
+
volumeRev: z.number().default(0).volatile(),
|
|
1042
1063
|
/** Preset played when a Session gains a pending approval or plan review. */
|
|
1043
1064
|
decisionSound: z.union(SOUND_IDS).default("bell").volatile(),
|
|
1044
1065
|
/** Preset played when a Session gains a pending question. */
|
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.2",
|
|
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",
|