← Back to home@guanhaisen

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:

TriggerWhen it fires
Needs attentionThe Agent is blocked waiting for your approval or your answer.
Run finishedA 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 pasteWhen to use it
github:guanhaisen/dsh-desktop-notifyStraight from this repository. Nothing published to npm.
https://github.com/guanhaisen/dsh-desktop-notifySame thing, written as a URL.
D:\path\to\dsh-desktop-notifyA 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:

SituationWhy it is silent
You are looking at that SessionThe 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 openedThe first snapshot is a baseline, so opening DSH does not fire a burst.
Windows Do Not Disturb is onThe 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 with standard: true and secure: true, making the window a secure context, which the Web Notification API requires.
  • The application session installs a permission handler that returns true for 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:

kindRaised byCopy shown
approval@deepseek-ai/dsh-client-ui-approvalTool named in the request is asking for privileged execution
question@deepseek-ai/dsh-client-ui-user-questionsThe Agent asked a question
plan-reviewsame, when the questions are a planA 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

FileRole
index.jsHost half. Owns the Loader row the browser half binds to. Registers nothing — no tool, no service, no route.
client.jsBrowser half. Watches Session status, decides when to notify, renders the toast, wires click-through, and publishes the DesktopNotify Inspect provider.
cordis.patch.ymlThe one Host row the bundle inserts.
test/watcher.test.mjsBehavioural 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:

KeyDefaultMeaning
notifyOnAttentiontrueNotify when a Session is blocked on you.
notifyOnCompletetrueNotify when a run finishes.
minRunMs20000Ignore runs shorter than this. Quick replies are not news.
soundtruePlay 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:

MethodAnswers
statusIs the half mounted? What is Notification.permission? Is this a secure context? Which Sessions are tracked, and since when?
testToastFire one real toast now, and report what was constructed.
decisionsThe 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.