dsh-desktop-notify
English | 中文
Native desktop notifications for DeepSeek Harness: permission requests can be answered directly from the system notification (Reject / Allow once), and turn completion, questions and errors can raise reminders; notification sounds support system sounds, a built-in chime, custom audio and silence.
Features
- Cross-platform native notifications: Windows (WinRT toast +
dshnotify:protocol callback), macOS (terminal-notifier), Linux (notify-send). - Inline notification buttons: permission notifications carry Reject and Allow once; clicking returns the decision to the Host's
approval/requestwaterfall. - Foreground fallback: by default the plugin takes over only while the window is not focused; when the window is focused, the user returns, or the notification times out, the request falls back to DSH's native approval panel.
- Other reminders: questions, turn completion and session errors; each switchable and optionally limited to the background.
- Away detection: a focused window with no keyboard or pointer input for the configured number of seconds counts as away (default 180 s, 0 disables).
- Session titles: notification bodies prefer the session title and fall back to the working directory name.
- Bell entry: the Notifications settings section uses a bell icon (0.2.2+ uses the native
settings.section.icon; 0.2.0/0.2.1 use a DOM patch that silently falls back to the default gear if the structure changes). - Custom notification sound:
system/bundled/custom/none, previewable in settings. - Interaction cue: approvals and questions (including plan prompts) play a separate cue, configured independently from the notification sound and regardless of window focus — it plays even while the window is focused; also supports
bundled/custom/noneand preview. - Reusable service: the Host registers
ctx.desktopNotify; other plugins can callshow()to display a notification with buttons. - Windows notification identity: automatically reads the AppUserModelID of the installed
DeepSeek Harness.lnk, so the toast header shows "DeepSeek Harness" and the application icon; falls back to a manually configured AUMID only when detection fails. - Windows protocol repair: the settings page can re-register the
dshnotify:protocol and open the system notification settings after confirmation.
Platform support
| Capability | Windows | macOS | Linux |
|---|---|---|---|
| Notification display | WinRT toast (PowerShell) | terminal-notifier | notify-send |
| Inline buttons | Yes (dshnotify: protocol + VBS) |
Yes | needs libnotify 0.7.10+ and a daemon with actions |
| Click callback | VBS writes a rendezvous JSON | stdout | notify-send -A |
| System sound | toast <audio ms-winsoundevent:...> |
system sound names (Bottle, etc.) | canberra-gtk-play, best effort |
| Custom audio | WAV only (SoundPlayer) |
formats supported by afplay |
paplay / canberra-gtk-play / aplay |
| Notification identity | DeepSeek Harness (reuses the installer shortcut) + app icon |
terminal-notifier (platform limitation) | app-name + icon |
| Interaction cue | Yes (SoundPlayer plays a built-in/custom WAV) |
afplay |
paplay / canberra-gtk-play / aplay |
On macOS notifications appear as terminal-notifier, not DeepSeek Harness. This is a known limitation of node-notifier/terminal-notifier 3 (sender/icon cannot be set). Buttons, sounds and click callbacks still work. For full branding you can build your own terminal-notifier (
make icon ICON=... APP_NAME=...) and set it under Settings → Advanced → macOS custom notifier path.
DSH compatibility
DSH currently ships as release candidates (0.2.0-rc.x, 0.2.1-rc.x, 0.2.2-rc.x); this plugin was audited and adapted against 0.2.0-rc.2 / 0.2.1-rc.5 / 0.2.2-rc.1, covering the intermediate RCs of 0.2.0/0.2.1/0.2.2.
| DSH | Settings nav icon | Config form | Notifications/approvals |
|---|---|---|---|
| 0.2.2-rc.1 | native settings.section.icon |
configForms + volatile fields |
unchanged |
| 0.2.1-rc.5 / 0.2.1-rc.4 | DOM patch | configForms + volatile fields |
unchanged |
| 0.2.0-rc.2 | DOM patch | configForms + volatile fields |
unchanged |
Adaptation notes:
- Peer ranges were widened to
@deepseek-ai/cordis ^4.0.0and@deepseek-ai/schemastery ^3.18.0, covering cordis~4.0.4/ schemastery~3.18.4in 0.2.0 and cordis~4.1.0/ schemastery~3.19.0in 0.2.2. approval/requestand its result vocabulary,user-questions/request,agent/status|error|disposed,sessionTitle.get,connection.requestRejection,typertGateway.hasLiveClient,configForms.get/mutate/whileServed,settings.sectionand volatile form filtering were checked against all three versions.displayReasonis a localized{ en, zh, ... }object in 0.2.x (not a string); the plugin picks the value for the current UI language.- 0.2.2 adds the
settings.section.iconslot; 0.2.0/0.2.1 do not have it, so the plugin falls back to its DOM patch and all three versions show the bell. configFormsis injected optionally: when a composition does not provide it, notifications, approvals and presence keep working; only the settings page is absent.
Install
Two options; pick one. Afterwards restart dsh web (or fully quit and reopen the desktop app), then press F5 in the browser; the configuration page appears under Settings → Notifications.
Option A: a complete resource bundle (local directory / archive) (recommended)
Use this when someone handed you a zip, or you already have an extracted plugin directory (it should contain package.json, cordis.patch.yml, lib/, assets/, README.md).
# 1) Extract to a stable directory you will not move or delete afterwards
# (never a temp/download directory). Example: D:\Plugins\dsh-desktop-notify
# Install the layer that contains package.json; do not nest another
# same-named directory.
# 2) Make sure the key files are present (missing assets/ means no icon/sound)
Test-Path "D:\Plugins\dsh-desktop-notify\package.json",
"D:\Plugins\dsh-desktop-notify\cordis.patch.yml",
"D:\Plugins\dsh-desktop-notify\lib\winrt.js",
"D:\Plugins\dsh-desktop-notify\assets\notify.wav"
# 3) Remove a previous installation of the same plugin to avoid version conflicts
dsh plugin --profile web remove dsh-desktop-notify
# 4) Install using the absolute path (quote paths that contain spaces)
dsh plugin --profile web add link:D:\Plugins\dsh-desktop-notify
Notes:
link:is a symlink install: edits in the source directory take effect immediately, but the directory must not be moved or renamed afterwards; if it is, runaddagain.- If the local directory has no
node_modules, runpnpm installin it before alink:install (macOS/Linux neednode-notifierfor terminal-notifier / notify-send; Windows uses WinRT and usually works without it). - To install a copy instead:
dsh plugin --profile web add file:D:\Plugins\dsh-desktop-notify(pnpm installs dependencies too; later changes in the source directory are not picked up and require anotheradd). - The
dsh-desktop-notify-<version>.tgzfrom the Releases page can also be installed directly:dsh plugin --profile web add file:D:\Downloads\dsh-desktop-notify-<version>.tgz.
Option B: install straight from GitHub
No local clone required, one command:
dsh plugin --profile web add github:HonestShine/dsh-desktop-notify
- To pin a release:
dsh plugin --profile web add github:HonestShine/dsh-desktop-notify#v0.1.1 - pnpm records the dependency as
github:HonestShine/dsh-desktop-notify#<commit sha>;dsh pluginautomatically adds packages that declaredsh.bundleto the profile'sdsh.profile.bundles, socordis.patch.ymldoes not need manual edits. - The plugin has no build scripts; pnpm's
prepareonly installs dependencies (node-notifier), so theallowBuildsprompt normally does not appear. - Requires Node.js
^22.22.2 || ^24.15.0 || >=26.0.0(thenode-notifier@12engine requirement, declared underenginesinpackage.json).
Notes
- The commands above use the
webprofile (the onedsh webboots). The desktop (Electron)desktopprofile is managed by the application itself; the CLI rejects--profile desktop, so use the app's built-inplugin_managerto install the same directory or tarball. - Notification service lifecycle: on load/enable the plugin registers the
dshnotify:protocol (registryHKCU\Software\Classes\dshnotify, with a per-instance nonce); on disable or uninstall it unregisters the protocol and cleans the%LOCALAPPDATA%\dsh-desktop-notifypolling directory. A plugin reload skips the unregistration of the old instance (nonce mismatch), so a fresh registration is never deleted. - Uninstall:
dsh plugin --profile web remove dsh-desktop-notify(the protocol is unregistered on disable/uninstall; no manual registry cleanup). - The plugin writes into a profile and affects every session in it; disabling/uninstalling restores the native behavior.
- If the settings page does not appear after the first install, restart
dsh weband press F5 again.
Usage
Permission approvals
- Move focus away from the window (switch to another app or minimize it).
- Trigger a tool action that needs approval.
- The system notification shows Reject / Allow once; clicking either continues or rejects the tool immediately.
- If you switch back to the DSH window, the notification is closed and the in-app approval panel takes over.
While the window is focused no notification appears and the native panel is used; the interaction cue still plays (see Sound below).
Other reminders
- Question reminder: fires when the agent calls
ask_user_question; the notification does not take over the Q&A, and clicking the body focuses the app. - Turn completion: fires on the root session's
running → idletransition. - Error reminder: fires on the root session's
agent/error; identical errors are deduplicated within 2 seconds.
Sound
The plugin has two kinds of sound, configured separately.
Notification sound (turn completion / error reminders / notifications with callback buttons) is selected under Settings → Notifications → Notification sound:
- System sound: Windows uses
Notification.Default(a system sound name can be set in the Notification sound group); macOS uses Bottle; Linux triescanberra-gtk-play. - Built-in chime:
assets/notify.wav, an original two-tone chime played through the platform player. - Custom audio file: an absolute path. Windows only supports
.wav; macOS/Linux depend on the system player. - Silent: no sound.
Interaction cue (approvals, questions/plan prompts and other interactions that need your reply) is configured separately in the Interaction cue group:
- By default plays the built-in
assets/alert.wav(three rising short notes), clearly different from the notification chime; customaccepts a custom.wav;nonesilences it;- Not limited by window focus: it also plays while the window is focused and the in-app approval panel is shown (the key difference from the notification sound);
- The same prompt rings at most once per 800 ms, so an approval and a question arriving together do not stack; on Windows the interaction toast is muted at the same time to avoid a double sound.
The Preview button plays the current mode once.
Configuration
The configuration lives in the profile's cordis.patch.yml (written by the settings page), with entry id desktop-notify:
| Field | Default | Description |
|---|---|---|
enabled |
true |
Plugin master switch |
approval.enabled |
true |
Take over permission approval notifications |
approval.onlyWhenHidden |
true |
Take over only while the window is not focused |
approval.timeoutSeconds |
30 |
Notification wait limit (5–300) |
events.question |
true |
Question reminder |
events.turnEnd |
true |
Turn-completion reminder |
events.errors |
true |
Error reminder |
events.onlyWhenHidden |
true |
Reminders only in the background |
sound.mode |
bundled |
system / bundled / custom / none |
sound.file |
"" |
Absolute path to custom audio |
sound.systemName |
"" |
Windows/macOS system sound name override |
attention.enabled |
true |
Play the interaction cue on approvals/questions (also while focused) |
attention.mode |
bundled |
Interaction cue source: bundled / custom / none |
attention.file |
"" |
Absolute path to a custom interaction cue |
windows.appId |
"" |
Optional manual Windows AppUserModelID; empty auto-reads the registered DeepSeek Harness.lnk |
windows.duration |
long |
Windows toast display time (short / long); Windows may ignore it for toasts with buttons |
macos.customNotifierPath |
"" |
Optional custom terminal-notifier |
presence.staleSeconds |
120 |
Presence report expiry |
presence.idleSeconds |
180 |
Treat a focused window without input as away after this many seconds; 0 disables |
Troubleshooting
Windows notifications missing or buttons unresponsive
- Open Settings → System → Notifications and make sure both the notification master switch and DeepSeek Harness notifications are on, and that Focus assist / Do not disturb is not blocking them.
- The notification header should show DeepSeek Harness and the app icon. The plugin automatically reads the AppUserModelID of
DeepSeek Harness.lnkin the Start Menu (created by the installer); if it shows the raw AUMID or the wrong icon, verify that shortcut exists, or set an AppUserModelID override under Settings → Notifications → Advanced → Windows notification identity. - Normally the plugin registers
dshnotify:on load (and unregisters it on disable/uninstall); if buttons stop working, use "Repair notification button protocol" in the plugin settings to re-register. The registration lives underHKCU\Software\Classes\dshnotifyand points at the plugin'sassets/activate.vbs. - Clicking a button starts
wscript.exe, which writes a JSON file into%LOCALAPPDATA%\dsh-desktop-notify\actions; the Host polls that directory. If no new file appears, check whether Windows blockswscript.exeor security software blocks the custom protocol. - System sounds are played by the toast's
<audio ms-winsoundevent:...>; built-in/custom sounds are played bySoundPlayer. If neither plays, check the system notification sound settings.
Windows button callbacks depend on the Host process being alive; after the app quits, buttons in historical notifications are no longer processed. The protocol registration is written to the current-user registry and needs no administrator rights.
macOS
- The first notification asks for system permission; allow it under System Settings → Notifications → terminal-notifier.
- The notification identity is terminal-notifier (see above).
- A custom notifier path must point to an executable terminal-notifier.
Linux
- Requires
notify-send(libnotify-bin) and a notification daemon with action support; buttons neednotify-send --version≥ 0.7.10. - Without action support the plugin falls back to the in-app approval panel; the status page says so.
- A custom sound needs at least one of
paplay,canberra-gtk-playoraplay.
Architecture
- Host plugin (
index.js): registers the Cordis ServicedesktopNotify; listens forapproval/request(prepend + global),user-questions/request,agent/statusandagent/error; Windows useslib/winrt.js(WinRT toast + protocol callback +assets/resolve-aumid.ps1for the notification identity), macOS/Linux uselib/notifier.js(node-notifier);lib/sound.jsplays the notification sound and the interaction cue (two independentSoundPlayerinstances);lib/presence.jstracks multi-window focus. The interaction cue plays whenapproval/requestanduser-questions/requestarrive, without checking focus. - Client plugin (
client.js): always reports window focus, idle time and locale; registers the settings page undersettings.section, reads and writes Host configuration throughctx.configForms.get('desktop-notify'), and uses same-origin/desktop-notify/*routes for testing, preview, status and Windows protocol repair. 0.2.2+ registers the nativesettings.section.icon; 0.2.0/0.2.1 use a DOM patch (data-shortcut-modal="settings"+ a label<span>) and keep the shell's default gear if it fails. - HTTP routes:
/desktop-notify/status|presence|test|preview-sound|preview-attention|open-notification-settings|repair-windows, all gated byctx.connection.requestRejection(req), JSON body limited to 16 KiB. - Windows action flow:
assets/winrt-toast.ps1shows the toast; clicking<action activationType="protocol" arguments="dshnotify:?token=...&decision=...">makesassets/activate.vbswrite a rendezvous file; the Host polls every 300 ms and resolves the approval by token. - Approval coordination: pending approvals are delegated back to the native panel only while an active Client exists; without a Client the notification buttons are required, with a hard 2-minute cap that closes them as
unavailable. - No DSH core changes:
app.asar, Electron main/preload are untouched and no IPC is added.
Development
pnpm install
node --check index.js
node --check client.js
node test/unit.test.js # run node:test directly (do not use --test spawn under a sandbox)
node scripts/winrt-probe.mjs 300 # Windows: real WinRT toast + protocol button callback
node scripts/notify-probe.mjs 300 # macOS/Linux: node-notifier probe
node scripts/make-chime.mjs # regenerate assets/notify.wav and assets/alert.wav
Both probes wait for you to click a button; under the restricted DSH sandbox child processes / named pipes may be blocked, so run them in a normal terminal or an elevated sandbox. The Host process is not affected by that sandbox at runtime. Disable the plugin before running
winrt-probe.mjsso its watcher does not consume the callback file first.
Inspiration
The bell icon, idle-away detection and session-title body are interactions inspired by the open-source project dsh-notify-me (MIT). This project borrows interaction and implementation ideas only; the code is an independent implementation, and the two have different positioning:
- dsh-notify-me is mostly browser-layer and its desktop approval notifications carry no buttons; this plugin keeps both approvals and notifications in the Host, so Windows notifications carry Reject / Allow once and return the result through a custom protocol.
- Not yet included but worth considering: per-session mute, external push (Discord / Bark / Gotify / Feishu / ServerChan / WxPusher / Webhook), host-side fallback when the page is closed, notification title markers (🔔 / ✅).
- Navigation icon: 0.2.2+ uses the native
settings.section.icon; 0.2.0/0.2.1 can only use a DOM patch and fall back to the default gear if the host structure changes.
Known limitations
- macOS notifications cannot be branded as DeepSeek Harness (third-party notifier limitation).
- Windows uses WinRT plus a custom protocol; the first registration writes
HKCU\Software\Classes\dshnotify. If security software blockswscript.exe, button callbacks are unavailable. - Windows custom audio only supports WAV; system sound names must start with
Notification.. - Action labels are sanitized (
,,;and a leading-are removed; max 24 characters); action ids keep only letters, digits,_and-; at most 3 buttons. - Concurrent approvals: only one is taken over; the rest go to the in-app panel.
- Bell icon: 0.2.2+ uses the native slot; 0.2.0/0.2.1 use a DOM patch and silently fall back to the default gear if DSH changes the settings structure; notifications are unaffected.
- Idle detection starts when the page loads; if the page just loaded and the user is already away, the first reminder can be up to one threshold (default 180 s) late.
- Configuration changes reload the Host plugin; in-flight notifications are closed and pending approvals are delegated back to the panel.
- No notification center/history; nothing is re-sent after the app quits.
Versioning & releases
- Version history: CHANGELOG.md.
- Releases: GitHub Releases (since
v0.1.0); the plugin is not published to npm and keeps"private": trueto prevent accidental publishing.
Documentation
- English introduction: this file (
README.md). - Chinese introduction: docs/README.zh-CN.md.
License
Plugin code is MIT. Runtime dependencies:
- node-notifier — MIT
- SnoreToast (bundled with node-notifier) — LGPL-3
- terminal-notifier (bundled with node-notifier) — MIT
assets/notify.wav and assets/alert.wav are synthesized by scripts/make-chime.mjs; no third-party audio copyright applies.
No comments yet. Be the first to write one.