Bundle
dsh-notify-plus
Context-rich native notifications for DSH Desktop: the toast title is the live conversation title and the body is a one-line distillation of what the turn produced.
- Source
- hoyin-law
- License
- MIT
- Updated
- Updated 14 hours ago
Readme
# dsh-notify-plus
[English](README.md) | [中文](README.zh.md)
Context-rich native notifications for **DSH Desktop**.
DSH Desktop's built-in notification tells you that *a* turn finished. It never
tells you *which* conversation, or *what* the turn actually produced — so the
toast is impossible to act on:
| | Built-in | dsh-notify-plus |
| --- | --- | --- |
| Title | `用户回合已完成` / `User Turn Completed` | `重构通知插件` — the live conversation title |
| Body | `一个由你发起的回合已完成。` / `A user-initiated turn has finished.` | `已经修复了通知排序,现在会优先选择最近的助手…` — a distilled one-liner |
This plugin replaces the built-in row and reuses its settings namespace, so the
existing Desktop toggles keep working and no user preference is lost.
## What it does
- **Titles every toast with the live conversation title.** The title comes from
the log-backed session title service, so it follows renames and automatic
title revisions instead of being a constant.
- **Distils the turn into one actionable line.** For CJK replies the body is
10–20 characters; for Latin replies it stays inside the Windows toast body
envelope. Nothing is sent to a model — the distillation is deterministic, so
it costs no tokens and adds no latency.
- **Keeps failure reasons.** A failed turn reports the actual error instead of a
generic apology, and a context-exhaustion stop is labelled separately.
- **Carries the job label** on background job notifications, so `pnpm test`
finishing is distinguishable from several other jobs finishing at once.
- **Never fires for internal fan-out.** Delegated subagent sessions and turns
that no user message started are ignored, exactly like the built-in row.
## Requirements
| | |
| --- | --- |
| DSH Desktop | 2.0.x (verified against 2.0.3) |
| Harness | `@deepseek-ai/dsh 0.1.1-rc.x` |
| Node | `>=22.19.0` (only for development and tests) |
The plugin depends on one harness-provided module, `@deepseek-ai/schemastery`,
which DSH already mounts. You do not need to install it yourself.
## Install
### From the DSH Desktop Market
Search for `dsh-notify-plus` in the Desktop plugin market and install it. The
market writes the package into the active profile's `dsh.profile.bundles`, which
is all this plugin needs.
### From the command line
```powershell
dsh plugin --profile <your-profile> add dsh-notify-plus
```
`dsh plugin` is a pnpm forwarder: it installs the package into the profile and
reconciles `dsh.profile.bundles` against the installed state. A package that
declares `dsh.bundle.patch` — this one does — joins the layer stack
automatically.
### From a local checkout
```powershell
git clone https://github.com/hoyin-law/dsh-notify-plus.git
dsh plugin --profile <your-profile> add C:\path\to\dsh-notify-plus
```
Then restart DSH Desktop. Confirm the row is live before assuming anything:
```powershell
dsh --profile <your-profile> dump-config | Select-String "dsh-notify-plus"
```
## How the replacement works
Both rows are Cordis plugins. Rather than racing the built-in observer (which
would raise two toasts per turn), this plugin ships a bundle patch that disables
the built-in row and inserts its own:
```yaml
- id: desktop-notifications
disabled: true
- insert:
- id: dsh-notify-plus
name: dsh-notify-plus
```
The order matters and is what makes the override safe. DSH Desktop composes
patches as:
1. each layer in `dsh.profile.bundles`, in order — and the launcher splices its
own `dsh-plugin-desktop` layer in right after `@deepseek-ai/dsh-web-app`;
2. the active profile's `cordis.patch.yml`;
3. `$DSH_HOME/cordis.patch.yml`;
4. the launcher's computed per-install patches.
Third-party bundles come after the launcher's layer, so an id-targeted patch in
this package's `cordis.patch.yml` wins over `desktop-notifications`. Patching a
row that does not exist is a warning rather than an error, which is why the same
patch stays inert — instead of breaking boot — on a host that is not DSH
Desktop.
## Settings
The plugin registers the `dsh-desktop-notifications` namespace with the same
five keys DSH Desktop already renders, so **Settings → Notifications** controls
this plugin unchanged:
| Key | Default | Effect |
| --- | --- | --- |
| `enabled` | `true` | Master switch for every notification |
| `notifyOnTurnCompletion` | `true` | A conversation turn finished |
| `notifyOnTurnFailure` | `true` | A conversation turn failed or ran out of context |
| `notifyOnJobCompletion` | `true` | A background job finished |
| `notifyOnJobFailure` | `true` | A background job failed |
DSH Desktop already suppresses notifications while its window is focused, so
nothing fires for work you are watching.
### A known upstream caveat
On DSH Desktop releases that ship the `profile-preferences` record, edits made
in **Settings → Notifications** may revert on the next launch. The desktop keeps
a private copy of `mode`, `openBrowser`, `networkExposure`, and `notifications`
under `<userData>/profile-preferences/<profileHash>/state.json` and mirrors it
back into `settings.yaml` at startup, while the settings page only writes
`settings.yaml`. This plugin reads whatever the document says, so it inherits the
revert exactly as the built-in row does.
Tracked upstream as
[issue #947](https://github.com/anywhere-labs/deepseek-harness-desktop/issues/947).
DSH Desktop 2.0.3 does not contain that mechanism and is unaffected; the report
is against 2.0.9, and the mirroring code first appears in a later release than
2.0.3.
This is deliberately **not** worked around here. That record is another
component's private source of truth: writing it from a plugin would add a second
writer to a single-document record that also holds the user's browser and
network-exposure choices.
## What the body line is
`lib/summarize.js` is dependency-free and deterministic. It:
1. strips fenced code, inline code, link targets, bare URLs, HTML tags,
headings, list markers, blockquotes, emphasis, and control characters;
2. splits the remainder into sentences, keeping terminators attached;
3. drops a leading acknowledgement clause (`好的,`, `Sure,`) so the line starts
with the work rather than the pleasantry;
4. accumulates sentences until the script's minimum is met;
5. truncates at a clause boundary inside the budget, appending `…`.
Budgets live in one place and are easy to argue with:
```js
export const DEFAULT_LIMITS = Object.freeze({
cjk: Object.freeze({ min: 10, max: 20 }),
latin: Object.freeze({ min: 40, max: 140 }),
});
```
When a turn produced tool work but no prose at all, the body falls back to a
tool-call count (`已完成 3 项工具调用…`) rather than a hollow "done".
## Compatibility
This plugin reads harness internals that are not a stable public contract:
`desktopRuntime.notifyAttention`, `sessions.on("session/event")`,
`ctx.get("sessionTitle")`, and `jobs.onJobDone`. The wiring is pinned to DSH
Desktop 2.0.3 and is verified by tests against that shape.
If a harness release changes those service shapes, the plugin fails loud at boot
rather than silently dropping notifications — `desktopRuntime` is a declared
hard dependency, so the row waits for the Desktop shell instead of activating
blind. Please open an issue with your DSH Desktop version if that happens.
## DSH STORE status
This plugin is **not listed in [DSH STORE](https://dsh.store/)**. The store's
automated policy declines any bundle patch that disables a shipped entry, and
disabling one is exactly what makes this plugin work: without it, the built-in
`desktop-notifications` row keeps raising its own toast, and the two plugins
cannot coexist at all — both register the `dsh-desktop-notifications` settings
namespace, and `settings.register()` throws on a duplicate, so the second one
fails its row.
That is a deliberate, documented trade rather than an oversight:
- The row this plugin disables belongs to `dsh-plugin-desktop` (the DSH Desktop
launcher), not to a `@deepseek-ai/*` package. No official harness package,
entry ID, or namespace is modified.
- The replacement is exact: the plugin re-registers the same settings namespace
with the same five keys, so the Desktop switches keep working and no user
preference is lost.
- Install and start were verified end to end on DSH Desktop 2.0.3; see
[Verifying a live install](#verifying-a-live-install).
Install it from the repository instead:
```powershell
dsh plugin --profile <your-profile> add https://github.com/hoyin-law/dsh-notify-plus
```
A future release may make the disable an explicit opt-in in the user's own
profile layer, which would let the bundle patch become purely additive. That
needs the plugin to fall back to its own settings namespace when the built-in
row is still present, so it is a behaviour change rather than a packaging tweak.
### Upstream status
The capability this plugin adds is being implemented natively. Upstream `master`
still ships the hard-coded copy, but
[PR #969](https://github.com/anywhere-labs/deepseek-harness-desktop/pull/969)
adds session titles as notification headings plus a final-answer preview, and
[issue #951](https://github.com/anywhere-labs/deepseek-harness-desktop/issues/951)
asks for host-level governance of third-party plugin notifications.
[`docs/upstream-request.md`](docs/upstream-request.md) records the duplicate
check that stopped us filing a new request, the relevant threads, and what is
worth contributing to them.
If that work lands in a DSH Desktop release, this plugin should be retired
rather than kept alive by disabling an official row that is already correct.
### Declared compatibility
`package.json` carries a per-release declaration under
`dsh.compatibility.dshReleases`. Every entry is either runtime-verified or
interface-verified against the published package — never assumed:
| DSH release | Basis |
| --- | --- |
| `0.1.1-rc.2` | runtime-verified: installed into a live `web` profile on DSH Desktop 2.0.3 and observed raising correctly-shaped toasts |
| `0.1.5-alpha.2` | interface-verified: every consumed surface has an identical signature and payload shape |
| `0.1.5-rc.1` | interface-verified: same |
| `0.1.5-rc.2` | interface-verified: same |
"Interface-verified" means the four surfaces this plugin consumes —
`sessions.on("session/event")` with the `turn/start`, `turn/end`,
`assistant/message`, and `user/message` payloads; `sessionTitle.get(session)`;
`jobs.onJobDone(snapshot)` with `JobSnapshot.label`; and
`settings.register(ns, schema, options)` — were diffed against each release's
published types and sources and found unchanged. It is a source-level claim, not
a runtime acceptance, and it is labelled as such.
Only `win32` and the DSH Desktop host are declared. The notification path is
platform-neutral, but nothing else has been run.
## Development
```powershell
npm install # only needed for the harness-provided schemastery peer
npm test # 64 assertions across 4 suites, no harness required
npm run verify:layering # composes the real patch layers via the dsh CLI
```
The test suite covers the pure layers — summarisation, copy, session
projections, and the turn/job state machine — with hand-built event fixtures, so
the behavioural contract is checked without launching Electron.
`verify:layering` is the one check that needs DSH Desktop installed. It builds a
throwaway `DSH_HOME`, stands in for the launcher-owned layer, and asks the real
`dsh --dump-config` to compose the tree, asserting that the built-in row ends up
`disabled: true`, that this plugin's row follows it, and that a host without the
built-in row still exits `0` with only a warning. Point it elsewhere if your
installation is not in the default location:
```powershell
node scripts/verify-layering.mjs --app "D:\Apps\DSH Desktop\DSH Desktop.exe"
```
```
lib/
index.js Cordis wiring: settings, jobs, sessions observers
turn.js session event -> notification payload state machine
summarize.js deterministic text distillation
copy.js localized copy for every locale
session.js read-only projections over DSH session values
settings.js the shared dsh-desktop-notifications namespace
cordis.patch.yml bundle patch: disable the built-in row, insert this one
scripts/
verify-layering.mjs composes the real patch layers through the dsh CLI
tools/
asar.mjs reads harness interfaces out of app.asar
read-windows-toasts.mjs reads delivered toasts out of the Windows history DB
```
### Verifying a live install
`notifyAttention` returns early while the DSH Desktop window is focused, and it
raises an Electron `Notification` that leaves no application log line — so a
focused test can look like a no-op. The only reliable check is the shell's own
toast history:
```powershell
node tools/read-windows-toasts.mjs --limit 10
```
A working replacement looks like this, with the cutover exactly at the restart:
```
2026/9/14 04:33:35 title="重构通知插件" body="已经修复了通知排序逻辑,现在会优先…"
2026/9/14 04:23:24 title="用户回合已完成" body="一个由你发起的回合已完成。"
```
One row per turn, the title is a real conversation name, the body is a truncated
distillation rather than a fixed sentence, and the hard-coded row stops appearing
at the restart rather than alongside the new one — which is what proves the
built-in observer was disabled instead of merely joined.
## License
MIT
Install
dsh plugin --profile web add github:hoyin-law/dsh-notify-plus
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-notify-plus from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.