dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh 干完活之后通知你,【轻量级】通知插件,只通知,不交互,mac/windows/linux 操作系统支持,dsh 在干活时你可以摸鱼
English | 中文
Desktop notifications for DeepSeek Harness (dsh): get a system notification when a run finishes, and an immediate one when the model asks you a question (ask_user_question) or waits for approval (sandbox escalation / tool permission). The body reflects the result (completed / error / aborted / max-tokens / blocked / interrupted). Delivery is pluggable at bundle time: browser-native Notification() (default) or Tauri's @tauri-apps/plugin-notification.
The published npm package ships prebuilt browser assets — no local build permissions needed:
dsh plugin --profile web add dsh-notifier-plugin
You can also install the source straight from the git repo; the prepare script builds it automatically after install:
dsh plugin --profile web add github:hotpot-labs/dsh-notifier-plugin
pnpm ≥10 blocks git dependencies from running build scripts. If the install skips the build, allow it in the profile's
pnpm-workspace.yamland re-runadd:allowBuilds: dsh-notifier-plugin: true
For development; rebuild after edits:
dsh plugin --profile web add ./dsh-notifier-plugin
The install command runs pnpm add in the profile directory and appends packages that declare dsh.bundle to dsh.profile.bundles.
Verify the mount:
dsh --profile web --dump-config | grep -n dsh-notifier
pnpm run build:tauri (or DSH_NOTIFIER_BACKEND=tauri tsdown for just the client bundle).UNUserNotificationCenter rejects them outright, and tauri-plugin-notification swallows the delivery error. So on macOS the client posts to the host endpoint POST /dsh-notifier/notify instead; the host (dsh's node process) delivers via osascript, and banners appear under the system-signed "Script Editor" identity — no signing needed. Windows / Linux are unaffected and still use @tauri-apps/plugin-notification.tauri-plugin-notification in src-tauri/Cargo.toml + .plugin(tauri_plugin_notification::init()), and grant notification:default in its capabilities. Without it, permission requests fail and notifications are skipped (warned once in the console). The focus check behind foreground suppression (isFocused) is part of core:window:default (granted via core:default), so it needs no extra capability.Two ways, user layer wins over the entry config:
Web UI (recommended): open dsh web → Settings → Plugins → Plugin configuration → expand the notification card. Toggle the switches and Save.
Config file: declare the row in the profile's cordis.patch.yml (~/.dsh/profiles/<name>/cordis.patch.yml):
- id: dsh-notifier
name: dsh-notifier-plugin
config:
enabled: true # master switch
title: DeepSeek Harness
sound: true # allow the platform notification sound; false = silent request
onBlocked: true # master switch for blocking notifications
onQuestion: true # question notifications (only when onBlocked)
onApproval: true # approval notifications (only when onBlocked)
All fields are optional with the defaults shown above. A later layer replaces the whole config of the same-id row (no per-key deep merge).
dsh --profile web web) and hard-refresh the page.requestPermission().Notifications fire only while a dsh web page (or the Tauri desktop window) is open — delivery lives in the webview, so a headless CLI run with no connected page produces none.
| Event | Body |
|---|---|
| run completed | 任务已完成 - 任务:<session title> |
| run failed | 任务失败 - 任务:… |
| aborted / max-tokens / blocked / interrupted | 任务已中止 / 任务达到 token 上限 / 任务被阻塞 / 任务被中断 |
| question | 需要回答:<question text> — <session title> |
| approval | 需要批准:<toolName> — <reason> — <session title> |
The session title comes from the session list row (a host projection) — very early notifications may omit it.
requestPermission() call without a user gesture is silently denied by modern Chrome — if the first real notification doesn't prompt, click Send test notification once (a real click counts as the required gesture). If you previously denied it, re-enable notifications for the dsh origin in your browser's site settings.requestPermission() from the notification plugin; macOS may additionally ask at the OS level on first use.Even with the site permission granted, macOS decides whether banners actually appear:
Notification.permission // must be "granted"
new Notification('测试', { body: '手动测试' }) // granted but no banner => OS-level blocking
If the manual notification also shows nothing, the cause is in macOS settings, not the plugin.The plugin logs every delivery decision at debug level (filter the console by dsh-notifier-plugin): notify delivered means the notification was constructed successfully — if no banner follows, it's the OS/browser layer; notify skipped (permission) means the site permission is missing.
turn/end reason of each root session and fires once when the session flips from running to idle — a multi-round goal run produces a single notification with the true final result.ask_user_question, or when an approval request waits for you — controlled by onBlocked / onQuestion / onApproval.origin === 'subagent' in the session list), so one run produces one notification.visibilityState + hasFocus); in the Tauri shell it means the app window is focused (getCurrentWindow().isFocused()). The console logs notify suppressed (app in foreground); the settings card's Send test notification button bypasses this.The browser half of the plugin (src/client/) opens its own pair of host event streams (events.mux / events.host — the host implementation is multi-subscriber) and runs the notification state machines against the same session/event passthrough frames the host sees. The host half only owns the dsh-notifier settings namespace (schema + user layer), which both the settings card and the notification runtime resolve live.
Delivery is pluggable at bundle time via DSH_NOTIFIER_BACKEND:
| Variant | Build | Delivery |
|---|---|---|
| web (default) | pnpm run build | browser-native new Notification(title, { body }) |
| Tauri desktop shell | pnpm run build:tauri | @tauri-apps/plugin-notification (bundled into client.js) |
The unselected implementation never enters the module graph (#notifier-backend alias in tsdown.config.ts), so the web bundle contains no @tauri-apps/* code and vice versa. Tauri is a separate variant because its WKWebView does not support the browser Notification API.
pnpm install
pnpm run build # tsc (host, ESM) + tsdown (client, web variant) -> lib/
pnpm run build:tauri # tsc + tsdown with DSH_NOTIFIER_BACKEND=tauri
pnpm run typecheck # tsc --noEmit, host + client
pnpm test # vitest: result text / body building / state machines / watcher
npm pack --dry-run # the tarball ships only lib/ + cordis.patch.yml + README
src/index.ts host entry: name / Config schema / settings section
src/notifier.ts run-end state machine (fire once at idle) + blocking notifier
src/notify.ts result text + notification body building (platform-agnostic)
src/types.ts structural event types (no dsh internal packages)
src/client/index.ts client entry: settings card + watcher wiring + styles
src/client/watcher.ts notification runtime: mux/host stream subscription driving the state machines
src/client/backend.ts backend interface + build-time selection (#notifier-backend alias)
src/client/backend-browser.ts browser-native Notification() delivery
src/client/backend-tauri.ts @tauri-apps/plugin-notification delivery
src/client/NotificationCard.tsx / locale.ts settings card component + dictionaries
Q: Installed but no notification?
dsh --profile web --dump-config | grep dsh-notifier, and hard-refresh the page.requestPermission() without a user gesture is silently denied by modern Chrome).dsh-notifier-plugin: notify delivered = constructed fine, look at the OS layer; notify skipped (permission) = grant the site permission; notify suppressed (app in foreground) = you're already looking at the dsh page, working as intended.new Notification('测试') in the console; if that also shows nothing, it's the OS settings.tauri-plugin-notification and granted the capability (console shows a one-time warning otherwise).Q: How many notifications per run?
One. It fires only when the session flips from running to idle (the whole activity has converged), so multi-round goal runs don't spam; intermediate turns never fire early. Blocking interactions (question/approval) notify immediately, once each.
Q: The settings card doesn't appear?
The card only shows in a web profile after the web process restarts and the page is hard-refreshed.
License: MIT