dsh-desktop-notify
No description
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 17, 2026
- Updated
- Oct 17, 2026
Introduction
dsh-desktop-notify
English | 中文
Desktop notifications for DeepSeek Harness. A Windows toast appears when a Session needs you — and clicking it brings that Session back to the front.
Two triggers:
| Trigger | When it fires |
|---|---|
| Needs attention | The Agent is blocked waiting for your approval or your answer. |
| Run finished | A Session that was running has stopped, and the run lasted at least minRunMs. |
Install
Requires the Desktop application (the Windows toast path depends on Electron running the renderer; a browser tab has no AppUserModelID to notify under).
From the DSH plugin page (no terminal)
Open Settings → Plugins → Add plugin, and paste one of these into Package name or address:
| What you paste | When to use it |
|---|---|
github:guanhaisen/dsh-desktop-notify | Straight from this repository. Nothing published to npm. |
https://github.com/guanhaisen/dsh-desktop-notify | Same thing, written as a URL. |
D:\path\to\dsh-desktop-notify | A local checkout. Must be an absolute path. |
Then press Install. The plugin manager runs pnpm add in your profile, adds
the bundle to the profile's list, and the notification half activates without a
restart in most cases — reload the page (Ctrl+R) if the first toast does not
arrive.
Installing from git resolves the repository's default branch. If you fork this and your default branch differs, paste the full spec with a ref instead:
github:you/dsh-desktop-notify#main.
The plugin page states that installed plugins do not auto-update: to upgrade, uninstall and install again. A
github:install follows the default branch, so reinstalling picks up new commits.
From the command line
# In your DSH desktop profile directory:
# Windows: %USERPROFILE%\.dsh\profiles\desktop
pnpm add github:guanhaisen/dsh-desktop-notify
Or link a working copy, so edits apply on save:
pnpm add link:/path/to/dsh-desktop-notify
Either way, the package must then appear in that directory's package.json
bundle list — the plugin page does this for you; a manual install does not:
{
"dsh": {
"profile": {
"bundles": ["...existing bundles...", "dsh-desktop-notify"]
}
}
}
Keep this package free of runtime
dependencies. pnpm does not install a linked package's own dependencies, and a resolution failure during Cordis tree initialisation stops DSH from starting.
You will not see a toast when…
This is the part that looks like a bug and is not:
| Situation | Why it is silent |
|---|---|
| You are looking at that Session | The prompt is already on screen. |
The run was shorter than minRunMs (20 s) | A quick reply is not news. |
| The Session was already running when the window opened | The first snapshot is a baseline, so opening DSH does not fire a burst. |
| Windows Do Not Disturb is on | The OS suppresses the banner. The notification still reaches the notification centre. |
If none of those apply and nothing appears, read the decision log — see Verifying it actually works. It records every suppression with its reason, so "why did I not get a toast?" is answerable.
Why this is a plugin and not a shell patch
The obvious implementation — add a Notification call to the Electron main
process — cannot work here, and understanding why explains the whole design.
The Desktop Host is not the Electron main process. lib/main.js spawns it as
a separate Node child process:
spawn(this.node, ['--expose-internals', entry, ...], { stdio: ['ignore','pipe','pipe','ipc'] })
That child is the Electron executable re-entered in Node mode, so
require('electron') inside a Host plugin yields no Notification, and the
process holds no AppUserModelID. Meanwhile the shell's own notification code
(lib/main.js) only ever fires for one thing: a mandatory update.
The renderer, however, has everything needed:
dsh-app:is registered as a privileged scheme withstandard: trueandsecure: true, making the window a secure context, which the Web Notification API requires.- The application session installs a permission handler that returns
truefor every non-media permission.
So a plain Web Notification in the window becomes a real Windows toast under
the application's own identity (electron.app.DeepSeek Harness). No shell
patch, no helper process, and it survives application updates.
Where the state comes from
Nothing is polled and no second source of truth is created.
@deepseek-ai/dsh-client-ui-session already folds exactly these facts and
publishes them as ctx.uiSession.sessionStatus:
Map<sessionId, { running, pendingInteraction, completionUnread }>
It is fed by the api-session/status Remote event. This plugin only reads it.
pendingInteraction is the interaction object itself, whose kind is one of:
kind | Raised by | Copy shown |
|---|---|---|
approval | @deepseek-ai/dsh-client-ui-approval | Tool named in the request is asking for privileged execution |
question | @deepseek-ai/dsh-client-ui-user-questions | The Agent asked a question |
plan-review | same, when the questions are a plan | A plan is waiting for review |
Its key is the identity used to de-duplicate, so a status re-emission for the
same wait never produces a second toast.
Layout
| File | Role |
|---|---|
index.js | Host half. Owns the Loader row the browser half binds to. Registers nothing — no tool, no service, no route. |
client.js | Browser half. Watches Session status, decides when to notify, renders the toast, wires click-through, and publishes the DesktopNotify Inspect provider. |
cordis.patch.yml | The one Host row the bundle inserts. |
test/watcher.test.mjs | Behavioural test of the decision logic, runnable with plain node. |
Configuration
Set on the plugin's row in the profile patch, or via the Plugins page:
| Key | Default | Meaning |
|---|---|---|
notifyOnAttention | true | Notify when a Session is blocked on you. |
notifyOnComplete | true | Notify when a run finishes. |
minRunMs | 20000 | Ignore runs shorter than this. Quick replies are not news. |
sound | true | Play the system notification sound. |
De-duplication
A repeated interaction never re-notifies: identity is the interaction's own
key, so a status re-emission for the same pending approval stays silent. The
first snapshot after the window opens is treated as a baseline rather than as
news, which is what stops a launch from firing a burst of toasts for Sessions
that were already running.
Verifying it actually works
Delivery is asynchronous and visual, and neither half can observe the toast the
operating system renders. So the browser half publishes a Cordis Inspect
provider, DesktopNotify, which can be queried from outside the page:
| Method | Answers |
|---|---|
status | Is the half mounted? What is Notification.permission? Is this a secure context? Which Sessions are tracked, and since when? |
testToast | Fire one real toast now, and report what was constructed. |
decisions | The in-memory decision log — every notification sent and every one deliberately suppressed, with the reason. |
decisions is the one that answers "why did I not get a toast?": a suppressed
entry carries suppressed: 'watched' (you were looking at that Session) or
suppressed: 'too-short' (the run ended before minRunMs).
The log is in memory only, bounded to the last 100 entries. It is deliberately not written to disk: a file that can silently fail to appear is worse than no file at all.
Confirming against the OS
testToast reporting delivered: true only means a Notification was
constructed. To confirm it reached the operating system, compare Windows' own
record for this app's AUMID before and after:
$k = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\electron.app.DeepSeek Harness'
[DateTime]::FromFileTime([int64](Get-ItemProperty $k).LastNotificationAddedTime)
This is how the plugin was verified: the timestamp held steady across a 12-second
idle control, then advanced on each testToast.
Development
node --check index.js
node --check client.js
node test/watcher.test.mjs
The test loads client.js under a fake __ModuleLoader__, feeds it synthetic
Session-status transitions, and asserts which toasts fire and what the decision
log records. It exits non-zero on failure.
Constraints
Keep dependencies empty. The desktop profile installs this package with
pnpm's link: protocol, and pnpm does not install a linked package's own
dependencies. Declaring one would make dependency resolution fail during Cordis
tree initialisation and stop DSH from starting.