# dsh-notify [English](README.md) | [简体中文](docs/README.zh-CN.md) | [日本語](docs/README.ja.md) [![CI](https://github.com/knownothing114/dsh-notify/actions/workflows/ci.yml/badge.svg)](https://github.com/knownothing114/dsh-notify/actions/workflows/ci.yml) **A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin that raises a desktop notification whenever dsh needs your attention.** dsh itself gives no cue when it needs the human in the loop: a permission confirmation is pending, an `ask_user_question` prompt or a plan review is waiting for your answer, a turn/session has finished, a goal completed or got blocked, the agent hit an error, or a workflow run ended. When the terminal or the browser tab is in the background, all of these are easy to miss. This plugin listens to the dsh host event bus and fires a native desktop notification at exactly those moments — system notification (macOS / Linux), a popup (Windows), a terminal bell, or your own command. The plugin is a **pure observer**: every listener is passive. In particular, the `approval/request` listener only reminds you and **never decides for you** — it always forwards the request through `next()`, so the approval chain behaves exactly as if the plugin were not installed. ## Features | Trigger | Fires when | Default | |---|---|---| | `approval` | A permission-confirmation request (sandbox escalation, operations that need approval, …) | on | | `question` | `ask_user_question` prompt / `exit_plan_mode` plan review waiting for you | on | | `turnComplete` | A turn completes (the agent finished replying and is waiting for your next input) | on | | `goal` | A goal completes or gets blocked | on | | `error` | The session hits an error | on | | `workflow` | A `tool_workflow` run ends | on | Notification channels (`channel`): - `auto` (default): macOS → `osascript` system notification; Windows → PowerShell popup; other platforms → `notify-send` - `osascript` / `notify-send` / `powershell`: force a specific channel - `bell`: terminal bell (`\x07`) — zero-dependency fallback - `custom`: run your own command template (placeholders `{title}` `{body}` `{app}`) - `none`: disable Zero runtime dependencies beyond two `@deepseek-ai` packages (settings + schemastery). Works in any dsh profile (`web`, `headless`, custom). Every option is also editable from the web GUI: **Settings → 通知 (Notifications)** tab (see [Settings tab](#settings-tab-web)). Changes are written to the settings document and apply immediately — no restart. ## Installation Example for the `web` profile (any other profile works the same): ```sh # 1. Add the plugin to the profile's dependencies. # A local directory is linked into node_modules by pnpm; a git URL or an # npm package name also work. dsh plugin --profile web add /path/to/dsh-notify # or from this repository: dsh plugin --profile web add git+https://github.com/knownothing114/dsh-notify.git # or (once published to npm): dsh plugin --profile web add dsh-notify ``` The command installs the dependency **and registers the bundle automatically**. Verify that `dsh.profile.bundles` in the profile manifest contains `dsh-notify`, and add it manually if your dsh version did not: ```jsonc // ~/.dsh/profiles/web/package.json { "dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-notify" // ← add this line ] } } } ``` ```sh # 2. Restart dsh web. Plugins load at boot; HMR only hot-reloads config. ``` ## Configuration Sensible defaults are built in — usually nothing to change. To tune it, override the row by id `dsh-notify` in the profile's `cordis.patch.yml` (or a `--patch` overlay). An override **replaces the row's whole `config`**, and the plugin deep-merges partial input over its defaults, so you can write only the keys you want to change: ```yaml # ~/.dsh/profiles/web/cordis.patch.yml - id: dsh-notify config: channel: auto # auto | osascript | notify-send | powershell | bell | custom | none appName: dsh # source name shown in the notification ({app} in custom templates) sound: true # play a sound with the notification (macOS) minIntervalMs: 3000 # minimum interval between two notifications (burst guard) rootsOnly: true # only alert for root sessions; ignore subagents / background children verbose: false # also log every notification through the dsh logger customCommand: "" # command template when channel is custom, e.g.: # terminal-notifier -message {body} -title {title} triggers: approval: true question: true turnComplete: true goal: true error: true workflow: true enabled: true # master switch ``` ## Settings tab (web) With the `web` profile, the plugin registers a dedicated **通知 (Notifications)** tab in **Settings**. It shows every option from the section above and lets you change them with a staged form (Save / Discard / whole-section Reset): - master switch, channel, app name, sound, min interval, root-sessions-only, verbose logging, custom command - the six trigger switches Config precedence (highest wins): **settings document** (`$DSH_HOME/settings.yaml`, written by the tab, hot-applied) → **profile patch** (`cordis.patch.yml` `config:`) → built-in defaults. The host plugin reads the config live on every notification, so a change in the tab takes effect immediately. The browser half lives in `dist/client.js` (a prebuilt bundle in the dsh client-module format, served at `/plugins/dsh-notify/client.js`); the host half registers the `notify` settings namespace. A restart of `dsh web` is required when the plugin package is first installed or upgraded; afterwards the tab's changes need none. Note: dsh's API proxy only exposes *curated* settings namespaces to the web UI by default (model providers plus an explicit allowlist). The plugin therefore wraps the proxy's settings handlers so its `notify` namespace is readable and writable through the standard settings RPCs — every other namespace keeps the core allowlist behavior. ## How it works (event mapping) | Trigger | Listens to | |---|---| | `approval` | host-side `approval/request` waterfall (passive: `return next()`, never participates in the decision) | | `question` / plan review | `tool/call` inside `session/event` (tools `ask_user_question` / `exit_plan_mode`); the question text is extracted from the tool arguments | | `turnComplete` | `turn/end` inside `session/event` (`reason.kind === "completed"`) | | `goal` | `goal/changed` (operations `complete` / `block`); body carries the objective | | `error` | `agent/error` | | `workflow` | `tool-workflow/run-end` inside `session/event` (non-`completed` stop reasons alert too) | All listeners are registered at the root context, which — per dsh's scope-routing rules — receives every agent- and session-scoped event; `rootsOnly` filters out subagent noise through `ctx.agents.roots()`. Notifications are dispatched with detached `spawn` processes: they never block the agent loop, and failures are only logged. ## Uninstall ```sh dsh plugin --profile web remove dsh-notify # Also remove "dsh-notify" from dsh.profile.bundles in # ~/.dsh/profiles/web/package.json, then restart. ``` ## FAQ - **No notification?** Check that the resolved `channel` matches your system: on macOS, try `channel: osascript` explicitly (and `verbose: true` to see each notification in the dsh log); Linux needs `notify-send` (`libnotify`); also check Do Not Disturb and notification permissions. - **Too noisy?** Increase `minIntervalMs`, or switch off individual `triggers` entries. - **Subagents spamming?** Keep `rootsOnly: true`. - **Config change not applied?** `cordis.patch.yml` edits are hot-reloaded by HMR; adding/removing plugins or editing `package.json` bundles requires a restart of `dsh web`. ## Development ```sh cd dsh-notify npm install # installs @deepseek-ai/dsh-settings + @deepseek-ai/schemastery npm test # node --test: unit tests + apply() smoke tests + client form tests (30 cases) ``` ## Project layout ``` ├── lib/index.mjs host plugin (event listeners, settings namespace, apiProxy exposure) ├── dist/client.js browser bundle (Settings → 通知 tab), served at /plugins/dsh-notify/client.js ├── cordis.patch.yml bundle patch mounting the plugin row ├── docs/ │ ├── README.zh-CN.md 简体中文文档 │ ├── README.ja.md 日本語ドキュメント │ └── third-party-settings-namespace-exposure.md developer note: exposing a third-party │ settings namespace to the web UI ├── test/ node --test suites (unit, render, interaction, integration) └── package.json plugin manifest (exports, dsh.bundle / dsh.client declarations) ``` ## License [MIT](LICENSE)